Skip to content
This page has been auto-translated and may contain errors.View in English

Funciones ​

A medida que tus programas crecen, vas a terminar escribiendo la misma lógica en más de un lugar. Las funciones te permiten escribir esa lógica una sola vez, ponerle nombre y usarla en todas partes. Si la corriges en un solo lugar, cada llamada recibe la corrección automáticamente.

Las funciones son la unidad principal de reutilización y abstracción de código. Encapsulan un comportamiento, le dan un nombre, definen una interfaz clara (sus parámetros y su valor de retorno) y lo hacen invocable desde cualquier parte. Las funciones bien nombradas también funcionan como documentación: validate_email() te dice qué hace un bloque de código sin necesidad de leer su cuerpo.

En Python, una función es un objeto de primera clase: un valor que puedes pasar de un lado a otro como cualquier otro, así que se puede asignar a una variable, guardar en una lista o un dict, entregar como argumento a otra función y devolver desde una función. def crea ese objeto y lo vincula a un nombre dentro del scope actual (la región de código donde ese nombre es visible). Tratar a las funciones como valores es lo que hace posibles patrones como sorted(key=...) y las tablas pequeñas de plugins, y vale la pena familiarizarse con esta idea desde temprano.

python
def greet(name):
    return f"Hello, {name}!"

print(greet("Alicia"))   # "Hello, Alicia!"
print(greet("Roberto"))     # "Hello, Roberto!"

Escríbela una vez, úsala en todas partes, corrígela en un solo lugar.

Definir una función ​

La palabra clave def inicia la definición de una función, seguida por el nombre, paréntesis, dos puntos y un cuerpo indentado. Una función no hace nada hasta que la llamas. Defínela con def y luego llámala por su nombre con ().

def es una instrucción que crea un objeto función y lo vincula al nombre indicado dentro del scope actual. El cuerpo no se ejecuta en el momento de la definición; se ejecuta solo cuando se llama a la función. Las funciones sin una instrucción return devuelven implícitamente None.

def construye un objeto función y lo vincula al nombre dentro del scope actual. El cuerpo no se ejecuta cuando Python lee el def, se ejecuta solo cuando llamas a la función. Esa separación importa en la práctica: un error de tipeo dentro del cuerpo de una función (un nombre que no existe, por ejemplo) queda silencioso hasta que la función se llama de verdad, así que una función que nunca invocas puede esconder un NameError que solo aparece en producción el día que alguien la llama.

python
def say_hello():
    print("Hello!")

say_hello()   # llama a la función
JunoDefinir una función Escribe def nombre(): e indenta el cuerpo debajo. No pasa nada hasta que la llamas con nombre(), el def solo la prepara. Y si te olvidas del return, la función devuelve None silenciosamente, algo que me hizo tropezar más de una vez al principio.
JunoDefinir una funcióndef crea la función y la vincula a un nombre, pero el cuerpo solo se ejecuta cuando la llamas. Sin return, la función devuelve None. Tenlo presente cuando un resultado vuelva vacío sin una razón clara.
JunoDefinir una función El cuerpo no se ejecuta en el momento del def, solo cuando llamas a la función, así que un nombre roto adentro puede pasar desapercibido hasta que algo la llame de verdad. Una función sin return devuelve None, lo cual es su propio bug silencioso cuando quien la llama espera un valor.

Parámetros y argumentos ​

Los parámetros son las entradas que espera tu función. Los listas dentro de los paréntesis. Cuando llamas a la función, los valores que pasas se emparejan con los parámetros en orden.

Los parámetros definen la interfaz de una función. Los argumentos son los valores concretos que se pasan al momento de llamarla. Los argumentos posicionales se emparejan por posición; los argumentos con nombre (keyword) se emparejan por nombre. Los valores por defecto hacen que los parámetros sean opcionales.

Al momento de la llamada, los argumentos posicionales se vinculan de izquierda a derecha y los argumentos con nombre se vinculan por nombre. Pasar el mismo parámetro de las dos formas, o uno que Python no puede ubicar, lanza TypeError en la llamada, no dentro del cuerpo. Python también te permite restringir una firma: un * suelto hace que todo lo que viene después sea solo por nombre (quien llama debe nombrarlo), y un / hace que todo lo anterior sea solo posicional. Usa parámetros solo-por-nombre en flags booleanos y firmas amplias, donde un True posicional en el lugar equivocado es el tipo de error que compila sin problemas pero termina siendo un bug en producción.

python
def greet(name, greeting):
    print(f"{greeting}, {name}!")

greet("Alicia", "Hello")    # "Hello, Alicia!"
greet("Roberto", "Hi")         # "Hi, Roberto!"

Parámetro es el nombre en la definición de la función. Argumento es el valor real que pasas cuando la llamas. En la práctica, la gente usa las dos palabras indistintamente; la distinción importa principalmente cuando lees documentación.

JunoParámetros y argumentos Los parámetros son los nombres que listas en los paréntesis, los argumentos son los valores reales que entregas cuando llamas. Python los alinea de izquierda a derecha, así que el orden en que los pasas es el orden en que caen.
JunoParámetros y argumentos Los parámetros son los nombres en la definición, los argumentos son los valores en la llamada. Los posicionales se vinculan de izquierda a derecha, así que la posición es el contrato. Las dos palabras se usan indistintamente, la diferencia solo importa cuando lees documentación.
JunoParámetros y argumentos Los argumentos posicionales se vinculan de izquierda a derecha, los con nombre por nombre, y un choque entre ambos lanza TypeError justo en la llamada. Un * suelto obliga a quien llama a nombrar lo que sigue, que es la solución barata para una función cuyos flags True/False serían, de otro modo, una adivinanza en el sitio de la llamada.

Valores por defecto ​

Puedes darle a un parámetro un valor por defecto. Si quien llama no proporciona ese argumento, se usa el valor por defecto. Los parámetros con valores por defecto deben venir después de los parámetros sin valores por defecto.

Los valores por defecto hacen que los parámetros sean opcionales. Se evalúan una sola vez, al momento de la definición, no en cada llamada. Esto importa con los valores por defecto mutables: def f(items=[]) comparte la misma lista entre todas las llamadas. La solución es usar None como valor por defecto y crear la lista dentro del cuerpo de la función.

El detalle que te muerde: un valor por defecto se construye una sola vez, cuando se ejecuta el def, y es compartido por todas las llamadas que lo usan. Para un valor por defecto inmutable como 0 o "Hello" (uno que no puedes cambiar en el lugar) eso no es problema. Para un valor por defecto mutable como una lista o un dict (uno que sí puedes cambiar en el lugar) es una trampa, porque cada llamada que recurre al valor por defecto reutiliza el mismo objeto:

python
def add_item(item, items=[]):   # la lista se crea una sola vez, compartida para siempre
    items.append(item)
    return items

add_item("a")   # ['a']
add_item("b")   # ['a', 'b']  <- no es una lista nueva

La solución es la de siempre: usar None por defecto y construir un contenedor nuevo dentro del cuerpo.

python
def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items
python
def greet(name, greeting="Hello"):
    print(f"{greeting}, {name}!")

greet("Alicia")           # "Hello, Alicia!"
greet("Alicia", "Hi")     # "Hi, Alicia!"

Los parámetros con valores por defecto deben venir después de los parámetros sin valores por defecto.

JunoValores por defecto Dale a un parámetro un valor por defecto y se vuelve opcional, quien llama puede omitirlo. Los que tienen valor por defecto tienen que ir después de los obligatorios, ese orden es una regla que Python obliga a cumplir. Te ahorra escribir el mismo valor en cada llamada.
JunoValores por defecto Los valores por defecto hacen que un parámetro sea opcional y deben ir después de los obligatorios. El detalle: un valor por defecto se evalúa una sola vez en la definición, así que nunca uses uno mutable como []. Usa None por defecto y construye la lista dentro del cuerpo.
JunoValores por defecto Un valor por defecto se construye una sola vez cuando se ejecuta el def y se comparte entre llamadas, así que items=[] va acumulando datos silenciosamente entre llamadas. Usa None por defecto y crea uno nuevo en el cuerpo. Este error aparece en revisiones de código reales más que casi cualquier otro descuido de Python.

Argumentos con nombre ​

Al llamar a una función, puedes nombrar los argumentos. Esto hace que las llamadas sean más legibles, especialmente en funciones con muchos parámetros, y te permite pasarlos en cualquier orden.

Los argumentos con nombre hacen que las llamadas a funciones se auto-documenten. Puedes mezclar posicionales y con nombre: los argumentos posicionales deben venir primero. En funciones con flags booleanos o muchos parámetros de tipos similares, los argumentos con nombre evitan errores silenciosos por pasar argumentos en el orden equivocado.

Los argumentos con nombre se vinculan por nombre, así que se leen bien y te liberan de tener que recordar posiciones. La regla en el sitio de la llamada: los argumentos posicionales van primero, los con nombre después, y nombrar el mismo parámetro dos veces lanza TypeError. La jugada de diseño que vale la pena hacer es la inversa: pon un * suelto en tu propia firma para que quien llame tenga que nombrar un parámetro (def connect(host, *, timeout=30)). No cuesta nada y convierte un connect("db", 30) opaco en un connect("db", timeout=30) que sigue siendo legible incluso si alguien agrega un parámetro en el medio más adelante.

python
def describe_player(name, score, level):
    print(f"{name} | Score: {score} | Level: {level}")

describe_player("Alicia", 87, 5)                        # posicional
describe_player(name="Alicia", level=5, score=87)       # con nombre, en cualquier orden
describe_player("Alicia", level=5, score=87)            # mezcla: posicionales primero
JunoArgumentos con nombre Nombra los argumentos en la llamada (score=87) y la llamada se explica sola, además puedes pasarlos en cualquier orden. La única regla: los argumentos posicionales van antes que los nombrados. Ideal para funciones con un montón de parámetros.
JunoArgumentos con nombre Nombrar los argumentos hace que una llamada se auto-documente y sea independiente del orden, con los posicionales obligatorios primero. Para firmas amplias o flags booleanos, prefiere argumentos con nombre para que nadie tenga que contar posiciones al leer la llamada.
JunoArgumentos con nombre Los argumentos con nombre se vinculan por nombre y deben ir después de los posicionales. La verdadera palanca es tu propia firma: un * suelto obliga a quien llama a nombrar lo que sigue, así la llamada sigue siendo legible incluso después de que alguien inserte un nuevo parámetro en el medio.

Valores de retorno ​

return envía un valor de vuelta a quien llamó la función. Sin return, una función devuelve None. Una vez que se ejecuta return, la función termina de inmediato. Cualquier código que venga después en ese bloque se omite.

return sale de la función y pasa un valor a quien la llamó. Una función sin un return explícito devuelve implícitamente None. return puede aparecer en cualquier parte del cuerpo de la función y puede usarse varias veces; el primero que se alcanza termina la función. Esto hace que los retornos anticipados sean útiles como cláusulas de guarda.

return entrega un valor a quien llamó y termina la función ahí mismo, y una función que llega al final sin un return devuelve None. Tener varios return es buen estilo, no un síntoma de mal código: un return anticipado para el caso de fallo o el caso extremo al comienzo (el patrón de "cláusula de guarda") se lee mucho mejor que anidar todo el cuerpo dentro de un solo if grande. Un detalle sutil que conviene conocer antes de que te sorprenda: un return dentro de un bloque try de todas formas ejecuta primero el bloque finally correspondiente, así que la limpieza en finally ocurre incluso cuando retornas anticipadamente.

python
def add(a, b):
    return a + b

result = add(3, 4)   # result = 7
print(result)

return también sale de la función de inmediato. Cualquier código que venga después en ese bloque no se ejecuta.

JunoValores de retornoreturn entrega un valor a quien llamó la función, y la función se detiene ahí mismo, cualquier cosa después se omite. Si omites el return, obtienes None de vuelta. print muestra un valor, return lo entrega para poder usarlo, me tomó un rato sentir esa diferencia.
JunoValores de retornoreturn pasa un valor de vuelta y sale de inmediato, sin return significa None. Puedes tener varios, y un return anticipado para el caso extremo al comienzo suele leerse más limpio que envolver todo en un if gigante.
JunoValores de retornoreturn sale ahí mismo y llegar al final sin él da None. Apóyate en retornos anticipados como cláusulas de guarda en vez de anidar demasiado. Y recuerda que un return dentro de un try igual ejecuta el bloque finally, así que la limpieza ahí se dispara incluso en una salida anticipada.

Retornar varios valores ​

Python te permite retornar varios valores separándolos con comas. Quien llama los recibe como una tupla y puede desempaquetarlos en nombres separados en una sola línea.

Retornar varios valores con una coma los empaqueta en una tupla. Quien llama los desempaqueta con nombres correspondientes. Esto es idiomático en Python para funciones que naturalmente producen más de un resultado. No es una función especial; es empaquetado y desempaquetado de tuplas.

return a, b empaqueta los valores en una tupla, y quien llama los separa con low, high = f(). Se lee bien para dos o tres resultados. Más allá de eso, la posición se vuelve un riesgo: nadie que llame a la función recuerda si el tercer elemento era el conteo o el promedio, y un par intercambiado es un bug silencioso. En ese punto, es mejor retornar algo con nombre, un NamedTuple o un @dataclass, para que quien llame use result.average en vez de result[2]. De cualquier forma, anota el retorno como tuple[int, str] para que un type checker pueda detectar a quien desempaqueta la cantidad equivocada de valores.

python
def min_max(numbers):
    return min(numbers), max(numbers)

low, high = min_max([3, 7, 1, 9, 4])
print(low, high)   # 1 9

La sintaxis low, high = ... es desempaquetado: Python asigna cada valor retornado al nombre correspondiente.

JunoRetornar varios valores Separa los valores con una coma y la función los devuelve juntos como una tupla. Quien llama los recoge en una sola línea, low, high = min_max(...), un nombre por cada valor. Útil siempre que una función produzca naturalmente más de una respuesta.
JunoRetornar varios valores Una coma empaqueta los valores en una tupla, quien llama los desempaqueta con nombres correspondientes. Es simplemente empaquetado y desempaquetado de tuplas, no una función especial. Se ve limpio con dos o tres resultados, menos con más.
JunoRetornar varios valores El retorno con coma empaqueta una tupla, bien para dos o tres valores. Más allá de eso, la posición se vuelve una trampa que nadie puede leer, así que retorna un NamedTuple o un @dataclass y deja que quien llame use result.average en vez de contar índices.

Alcance (scope) ​

Las variables creadas dentro de una función existen solo dentro de esa función. No puedes verlas desde afuera. Las variables definidas fuera de todas las funciones son visibles en todas partes, pero no puedes cambiarlas desde dentro de una función sin una declaración explícita.

Un nombre creado dentro de una función es local: vive solo ahí y desaparece cuando la función termina. Un nombre definido en el nivel superior del archivo es global. Leer una variable global desde dentro de una función funciona sin ceremonias, pero asignarle un valor necesita global nombre, de lo contrario Python trata la asignación como la creación de una nueva variable local que oculta a la global. En la práctica, casi nunca necesitas global: pasar valores como argumentos y devolver resultados con return mantiene visibles los efectos de una función en el sitio de la llamada.

Python resuelve un nombre con la regla LEGB, revisando cuatro lugares en orden: Local (esta función), Enclosing (una función externa que envuelve a esta), Global (el módulo), y luego Built-in (nombres como len). Leer recorre esa cadena; asignar siempre crea una variable local a menos que indiques lo contrario. Esa asimetría es el origen del bug: count += 1 contra una variable global sin una línea global count lanza UnboundLocalError, porque la asignación marca a count como local para toda la función y la lectura ocurre antes de que exista ningún valor local. global nombre hace que una asignación vuelva a apuntar al nivel del módulo; nonlocal nombre apunta en cambio a la función envolvente más cercana, que es como un closure actualiza una variable de la función que lo creó. Trata ambos como una señal de alerta: una función que se estira para modificar el estado externo es más difícil de probar que una que toma entradas y devuelve salidas.

JunoAlcance (scope) Una variable creada dentro de una función vive solo ahí, el exterior no puede verla. Puedes leer una variable externa desde dentro, pero cambiarla necesita antes una línea global. Casi nunca necesitas eso: pasa valores como entrada, devuelve resultados, y la función se mantiene fácil de seguir.
JunoAlcance (scope) Los nombres creados dentro de una función son locales y desaparecen cuando termina. Leer una global es gratis, escribir una necesita global nombre o Python crea una local que la oculta. Prefiere argumentos de entrada y return de salida para que los efectos de una función se mantengan visibles en la llamada.
JunoAlcance (scope) LEGB para las búsquedas, pero la asignación siempre crea una local a menos que declares global o nonlocal, y por eso count += 1 sobre una global lanza UnboundLocalError. Ambas palabras clave son una señal de alerta: una función que modifica el estado externo es más difícil de probar que una que toma entradas y devuelve salidas.
python
def calculate():
    result = 42   # local a esta función
    return result

calculate()
print(result)   # NameError, result no existe aquí afuera
python
count = 0

def increment():
    global count    # declara que quieres modificar la variable global
    count += 1

increment()
print(count)   # 1

Usar global debería ser el último recurso. Hace que el código sea más difícil de razonar. Prefiere pasar valores como entrada y devolverlos como salida. El alcance se apoya directamente en cómo la asignación vincula un nombre a un valor, tema que se cubre en el capítulo Variables y tipos.

*args y **kwargs ​

A veces no sabes cuántos argumentos va a recibir una función. *args recolecta cualquier cantidad de argumentos posicionales en una tupla. **kwargs recolecta cualquier cantidad de argumentos con nombre en un diccionario. Los nombres args y kwargs son convenciones; los asteriscos son lo que realmente importa.

*args recolecta los argumentos posicionales sobrantes en una tupla. **kwargs recolecta los argumentos con nombre sobrantes en un dict. Ambos se pueden combinar con parámetros normales. Los parámetros normales van primero, luego *args, luego los parámetros solo-por-nombre, luego **kwargs. Son útiles para funciones envoltorio que pasan argumentos hacia otra función.

*args reúne los argumentos posicionales sobrantes en una tuple, **kwargs reúne los argumentos con nombre sobrantes en un dict. El movimiento inverso ocurre en el sitio de la llamada: func(*alguna_lista) distribuye una lista como argumentos posicionales y func(**algún_dict) distribuye un dict como argumentos con nombre. Esa simetría es lo que hace que funcione un envoltorio de paso directo, def timed(*args, **kwargs): return inner(*args, **kwargs) reenvía lo que recibió sin nombrar un solo parámetro. Es útil, pero te cuesta la legibilidad de la firma: un envoltorio que absorbe todo en *args, **kwargs no le muestra a quien llama ni al type checker nada sobre lo que acepta, así que úsalo cuando realmente necesites reenviar argumentos arbitrarios, no como la forma predeterminada de una función común.

python
def total(*args):
    return sum(args)

total(1, 2, 3)          # 6
total(1, 2, 3, 4, 5)   # 15
python
def display(**kwargs):
    for key, value in kwargs.items():
        print(f"{key}: {value}")

display(name="Alicia", score=87, level=5)

Puedes combinarlos con parámetros normales. Los parámetros normales van primero:

python
def describe(title, *tags, **metadata):
    print(f"{title} | tags: {tags} | meta: {metadata}")

describe("Introducción a Python", "beginner", "python", author="Alicia", year=2024)
Juno*args y **kwargs*args junta cualquier cantidad de argumentos posicionales extra en una tupla, **kwargs junta los extra con nombre en un dict. Las palabras args y kwargs son solo convención, el * y el ** hacen el trabajo real. Útil cuando no puedes saber de antemano cuántas cosas van a llegar.
Juno*args y **kwargs*args recolecta los argumentos posicionales sobrantes en una tupla, **kwargs los con nombre sobrantes en un dict. Orden en la firma: parámetros normales, luego *args, luego solo-por-nombre, luego **kwargs. Más útil en envoltorios que reenvían argumentos directo a otra función.
Juno*args y **kwargs* y ** recolectan en una firma y distribuyen en un sitio de llamada, que es lo que permite que un envoltorio reenvíe todo con inner(*args, **kwargs). El costo es una firma que no le dice nada a quien llama, así que resérvalo para reenvíos genuinos, no para funciones comunes.

Docstrings ​

Un docstring es una cadena de texto al comienzo de una función que describe qué hace. Los editores y herramientas de Python la usan para mostrar ayuda cuando pasas el cursor sobre una llamada a la función. Usa comillas triples, y escribe una sola línea para funciones simples.

Un docstring es lo primero en el cuerpo de la función, una cadena entre comillas triples. Las herramientas la leen: help(func) la imprime, y tu editor la muestra cuando pasas el cursor sobre una llamada, así que resulta útil justo cuando te has olvidado de qué hace la función. La convención es una línea de resumen, seguida de una línea en blanco y más detalle si es necesario. Trátalo como obligatorio en cualquier cosa que se llame desde más de un lugar, y opcional en un helper descartable cuyo nombre ya lo dice todo.

Un docstring es la primera instrucción en el cuerpo de una función, clase o módulo, y debería decir qué hace la función y qué devuelve, no repetir la lista de parámetros que el lector ya puede ver. Se justifica en cualquier cosa cuyo contrato no sea visible desde la firma: los casos extremos, qué excepciones lanza, qué hace con una entrada vacía. Omite el bloque formal argumento por argumento cuando los type hints de la firma ya cargan con los tipos; ese detalle pertenece a los hints, no duplicado en un texto que con el tiempo queda desactualizado. El objetivo es la línea que te gustaría leer a las 2am mientras depuras una llamada que escribiste hace seis meses.

python
def normalise(value, min_val, max_val):
    """Scale a value to the 0-1 range given the known min and max."""
    return (value - min_val) / (max_val - min_val)
python
def build_url(base, version, resource, *, secure=True):
    """
    Build an API endpoint URL.

    Returns a fully-qualified URL string. If secure is False,
    the URL will use http instead of https.
    """
    scheme = "https" if secure else "http"
    base = base.replace("https://", "").replace("http://", "")
    return f"{scheme}://{base}/{version}/{resource}"

Escribe un docstring para cualquier función cuyo propósito no sea evidente a partir de su nombre y su firma.

JunoDocstrings Un docstring es una cadena entre comillas triples justo al comienzo de una función, describiendo qué hace. Tu editor la muestra cuando pasas el cursor sobre la función más adelante, que es justo cuando te vas a alegrar de que esté ahí. Escribe uno siempre que el nombre y las entradas no dejen el propósito claro por sí mismos.
JunoDocstrings Primera línea del cuerpo, entre comillas triples: help() y tu editor la muestran al pasar el cursor. Una línea de resumen cubre la mayoría de las funciones, agrega detalle debajo solo cuando el comportamiento lo necesita. Obligatorio en cualquier cosa llamada desde varios lugares, omitible en una función de una sola línea cuyo nombre ya lo dice todo.
JunoDocstrings Dice qué hace la función, qué devuelve, y los casos extremos, no una lista de parámetros repetida que el lector ya puede ver. Deja que los type hints carguen con los tipos para no mantener los mismos datos duplicados dos veces. Apunta a la línea que querrías leer a las 2am depurando una llamada que escribiste hace medio año.

Type hints ​

Los type hints te permiten anotar qué tipos espera y devuelve una función. Python no los hace cumplir en tiempo de ejecución, pero los editores los usan para detectar errores antes de que ejecutes nada. El -> antes de los dos puntos especifica el tipo de retorno.

Los type hints son documentación que las herramientas pueden verificar. Los editores y los type checkers (mypy, pyright) los usan para detectar discrepancias de tipos antes de la ejecución. No tienen ningún efecto en tiempo de ejecución en Python estándar. -> None es la anotación correcta para funciones sin valor de retorno. Para contenedores genéricos, usa list[int], dict[str, int] (Python 3.9+).

Los type hints no cambian nada en tiempo de ejecución, Python corre igual con ellos o sin ellos. Todo su valor está en la capa externa al programa: un type checker (mypy o pyright) leyendo el código antes de que corra, y tu editor marcando una llamada incorrecta mientras escribes. Por eso un hint que miente es peor que ningún hint, porque el checker confía en él. Usa list[int], dict[str, int] y str | None directamente (no necesitas importar typing en Python moderno), y anota una función que no devuelve nada como -> None, lo cual también indica que llamarla dentro de una expresión (x = log(...)) es un error. El beneficio se nota en las funciones llamadas desde muchos lugares: el checker detecta a quien pasa el tipo equivocado en lugar de que eso aparezca como un fallo confuso tres niveles más abajo en tiempo de ejecución.

python
def greet(name: str, score: int) -> str:
    return f"{name} scored {score}"
python
def log(message: str) -> None:
    print(f"[LOG] {message}")
python
def top_scores(scores: list[int], n: int) -> list[int]:
    return sorted(scores, reverse=True)[:n]

Los type hints son opcionales pero valiosos en cualquier función que se llame desde varios lugares. Son documentación que las herramientas pueden verificar.

JunoType hints Los type hints anotan qué recibe y qué devuelve una función, como name: str y -> str para el retorno. Python no los va a hacer cumplir cuando corre, pero tu editor los lee y te avisa antes de que ejecutes. Piensa en ellos como notas que las herramientas pueden revisar.
JunoType hints Los hints son documentación que un type checker puede verificar, sin efecto en tiempo de ejecución. -> None es la anotación correcta cuando una función no devuelve nada, y list[int] / dict[str, int] funcionan sin importar typing en Python moderno. Vale la pena en cualquier cosa llamada desde varios lugares.
JunoType hints Los hints no hacen nada en tiempo de ejecución, su valor está en el checker y el editor que los leen, así que un hint que miente es peor que ninguno. Usa str | None y list[int] directamente, -> None cuando no hay retorno. La ganancia es detectar a quien llama con el tipo equivocado antes de que falle tres niveles más abajo.

Funciones como valores ​

Las funciones en Python son valores, como las cadenas o los números. Puedes asignarlas a variables y pasarlas a otras funciones. Así es como sorted() acepta una función key=.

Las funciones son objetos de primera clase: tienen un tipo (function), se pueden guardar en variables y colecciones, y se pueden pasar como argumentos o devolver como valores. Esta es la base de las funciones de orden superior como sorted(key=...), map() y filter().

Una función es un objeto que pasas por referencia, nunca una copia, así que entregarla a otra función es barato. El patrón que surge de esto es el closure: una función definida dentro de otra que sigue teniendo acceso a las variables de la función externa incluso después de que esta última haya terminado. Es la forma de construir una función configurada al vuelo, un make_multiplier(3) que devuelve una función que multiplica por 3, con el 3 capturado y retenido. Dos precauciones para código real: un closure captura la variable, no su valor en el momento de la captura, lo cual atrapa a quienes construyen closures dentro de un ciclo (todos terminan viendo el valor final del ciclo); y apilar muchas funciones devueltas hace que las rutas de llamada sean difíciles de rastrear, así que usa closures donde se lean con claridad y recurre a una clase cuando el estado crece demasiado.

python
def double(x):
    return x * 2

def apply(func, value):
    return func(value)

apply(double, 5)   # 10

Pasar funciones como argumentos aparece constantemente con sorted(), map() y filter(). También lo vas a ver en el capítulo Lambdas y comprehensions.

JunoFunciones como valores Una función es un valor, como una cadena o un número, así que puedes guardarla en una variable y entregarla a otra función. Eso es exactamente lo que te permite pasar una función como key= a sorted(). Me pareció raro al principio, luego hizo clic y empezó a aparecer en todas partes.
JunoFunciones como valores Las funciones son objetos de primera clase: las asignas, las guardas en una lista, las pasas, las devuelves. Ese es el motor detrás de sorted(key=...), map() y filter(), y detrás de cada callback que vas a conectar más adelante.
JunoFunciones como valores Pasar una función es una referencia, no una copia, y un closure permite que una función interna siga teniendo acceso a las variables de la externa después de que esta termine. Captura la variable, no el valor, así que construir closures dentro de un ciclo muerde siempre. Cuando el estado capturado crece, una clase se lee mejor.

En la práctica ​

Dos funciones que trabajan juntas: letter_grade convierte una nota en una letra, y summarise la llama para cada nota en una lista:

python
def letter_grade(score: int) -> str:
    if score >= 90:
        return "A"
    elif score >= 80:
        return "B"
    elif score >= 70:
        return "C"
    else:
        return "F"

def summarise(scores: list[int]) -> None:
    total = sum(scores)
    avg = total / len(scores)
    grades = []
    for s in scores:
        grades.append(letter_grade(s))
    print(f"Average: {avg:.1f}")
    print(f"Grades: {', '.join(grades)}")

summarise([87, 92, 74, 65, 91])

Un formateador de logs y un procesador de archivos que lo usa, con un parámetro por defecto dry_run que evita efectos secundarios a menos que se desactive explícitamente:

python
def format_log(level: str, message: str) -> str:
    return f"[{level.upper():5}] {message}"

def process_file(path: str, dry_run: bool = True) -> bool:
    print(format_log("info", f"Processing {path}"))
    if dry_run:
        print(format_log("info", "Dry run, no changes made"))
        return True
    return True

process_file("report.csv")
process_file("report.csv", dry_run=False)

Un normalizador de un solo valor y un normalizador de columna construido encima de él, con type hints y un docstring. La función de columna calcula el rango una sola vez y reutiliza la función escalar para cada elemento:

python
def normalise(value: float, min_val: float, max_val: float) -> float:
    """Scale value to the 0-1 range given known min and max."""
    if max_val == min_val:
        return 0.0
    return (value - min_val) / (max_val - min_val)

def normalise_column(values: list[float]) -> list[float]:
    """Normalise an entire column of values."""
    lo, hi = min(values), max(values)
    return [normalise(v, lo, hi) for v in values]

raw = [10.0, 25.0, 5.0, 40.0, 15.0]
print(normalise_column(raw))

Los type hints aquí cumplen dos propósitos: documentan lo que espera la función, y le permiten a un type checker detectar a quienes llaman pasando por error una lista de cadenas.