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

Funciones

docs.scrimba.com

A medida que tus programas crecen, escribirás la misma lógica en más de un lugar. Las funciones te permiten escribir la lógica una vez, nombrarla, y usarla en todas partes. Corrígela en un lugar y cada llamada obtiene la corrección automáticamente.

Las funciones son la unidad principal de reutilización y abstracción de código. Envuelven un comportamiento, le dan un nombre, definen una interfaz clara (sus parámetros y valor de retorno), y lo hacen invocable desde cualquier lugar. Las funciones bien nombradas también se leen como documentación: validate_email() te dice qué hace un bloque de código sin leer el 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 puede ser asignada a una variable, almacenada en una lista o diccionario, pasada a otra función como argumento, y devuelta desde una. def crea ese objeto y lo vincula a un nombre en el alcance actual (la región del código donde ese nombre es visible). Tratar funciones como valores es lo que hace posibles patrones como sorted(key=...) y pequeñas tablas de plugins, y vale la pena acostumbrarse temprano.

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

print(greet("María"))   # "Hello, María!"
print(greet("Carlos"))     # "Hello, Carlos!"

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

Definir una función

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

def es una sentencia que crea un objeto función y lo vincula al nombre dado en el alcance actual. El cuerpo no se ejecuta en el momento de la definición; se ejecuta solo cuando llamas la función. Las funciones sin una sentencia return devuelven implícitamente None.

def construye un objeto función y lo vincula al nombre en el alcance actual. El cuerpo no se ejecuta cuando Python lee el def, se ejecuta solo cuando llamas la función. Esa división importa en la práctica: un error tipográfico dentro del cuerpo de una función (un nombre que no existe, digamos) permanece silencioso hasta que la función se llame realmente, así que una función que nunca llamas puede ocultar un NameError que solo sale a la superficie en producción el día que alguien la llama.

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

say_hello()   # llama la función
JunoDefinir una función Escribe def name(): e indenta el cuerpo debajo. Nada ocurre hasta que la llames con name(), el def solo la configura. Y si olvidas un return, la función silenciosamente devuelve None, eso me atrapó 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 significa que la función devuelve None. Ten eso en mente cuando un resultado vuelva vacío sin una razón clara.
JunoDefinir una función El cuerpo no se ejecuta en el momento de def, solo cuando llamas la función, así que un nombre roto dentro puede permanecer desapercibido hasta que algo realmente la llame. Una función sin return devuelve None, que es su propio error silencioso cuando una llamada espera un valor.

Parámetros y argumentos

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

Los parámetros definen la interfaz de una función. Los argumentos son los valores concretos pasados en el momento de la llamada. Los argumentos posicionales se emparejan por posición; los argumentos nombrados se emparejan por nombre. Los valores predeterminados hacen los parámetros opcionales.

En el momento de la llamada, los argumentos posicionales se vinculan de izquierda a derecha y los argumentos nombrados se vinculan por nombre. Pasar el mismo parámetro de ambas maneras, o uno que Python no pueda colocar, lanza TypeError en la llamada, no dentro del cuerpo. Python también te permite restringir una firma: un * desnudo hace que todo después de él sea solo por nombre clave (las llamadas deben nombrarlo), y un / hace que todo antes de él sea solo posicional. Recurre a solo por nombre clave en banderas booleanas y firmas amplias, donde un True posicional en el lugar equivocado es el tipo de error que compila bien y envía un bug.

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

greet("María", "Hola")    # "Hola, María!"
greet("Carlos", "Saludos")         # "Saludos, Carlos!"

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 palabras indistintamente; la distinción importa principalmente cuando lees documentos.

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 el que los pasas es el orden en el 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 brecha solo importa cuando lees documentos.
JunoParámetros y argumentos Los argumentos posicionales se vinculan de izquierda a derecha, los nombrados por nombre, y un choque lanza TypeError justo en la llamada. Un `*` desnudo obliga a los llamadores a nombrar lo que sigue, que es la corrección barata para una función cuyas banderas True/False serían de otra manera un juego de adivinanzas en el sitio de la llamada.

Valores predeterminados

Puedes darle a los parámetros un valor predeterminado. Si el llamador no proporciona ese argumento, se usa el predeterminado. Los parámetros con valores predeterminados deben venir después de los parámetros sin valores predeterminados.

Los valores predeterminados hacen los parámetros opcionales. Se evalúan una vez en el momento de la definición, no en cada llamada. Esto importa para los predeterminados mutables: def f(items=[]) comparte la misma lista en todas las llamadas. La corrección es usar None como predeterminado y crear la lista dentro del cuerpo de la función.

El detalle que muerde: un valor predeterminado se construye una vez, cuando def se ejecuta, y se comparte por cada llamada que lo usa. Para un predeterminado inmutable como 0 o "Hola" (uno que no puedas cambiar en su lugar) eso está bien. Para un predeterminado mutable como una lista o diccionario (uno que puedas cambiar en su lugar) es una trampa, porque cada llamada que cae al predeterminado reutiliza el mismo objeto:

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

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

La corrección es la estándar: predetermina a None, luego construye un contenedor fresco 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="Hola"):
    print(f"{greeting}, {name}!")

greet("María")           # "Hola, María!"
greet("María", "Saludos")     # "Saludos, María!"

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

JunoValores predeterminados Dale a un parámetro un predeterminado y se vuelve opcional, el llamador puede omitirlo. Los predeterminados tienen que venir después de los requeridos, ese orden es una regla que Python impone. Te ahorra escribir el mismo valor en cada llamada.
JunoValores predeterminados Los predeterminados hacen un parámetro opcional y deben sentarse después de los requeridos. La trampa: un predeterminado se evalúa una vez en la definición, así que nunca uses uno mutable como []. Predetermina a None y construye la lista dentro del cuerpo en su lugar.
JunoValores predeterminados Un predeterminado se construye una vez en el tiempo de def y se comparte en las llamadas, así que items=[] silenciosamente se acumula entre llamadas. Predetermina a None y haz una fresca en el cuerpo. Este aparece en la revisión de código real más que casi cualquier otro desliz de Python.

Argumentos nombrados

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

Los argumentos nombrados hacen las llamadas a funciones autodocumentadas. Puedes mezclar posicionales y nombrados: los argumentos posicionales deben venir primero. Para funciones con banderas booleanas o muchos parámetros de tipos similares, los argumentos nombrados evitan errores silenciosos de pasar argumentos en el orden equivocado.

Los argumentos nombrados se vinculan por nombre, así que se leen bien y te liberan de recordar posiciones. La regla en el sitio de la llamada: los argumentos posicionales vienen primero, los nombrados después, y nombrar el mismo parámetro dos veces lanza TypeError. El movimiento de diseño que vale la pena hacer es el inverso: pon un * desnudo en tu propia firma para que los llamadores deban nombrar un parámetro (def connect(host, *, timeout=30)). No cuesta nada y convierte un opaco connect("db", 30) en un connect("db", timeout=30) que sobrevive a alguien añadiendo un parámetro en el medio después.

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

describe_player("María", 87, 5)                        # posicional
describe_player(name="María", level=5, score=87)       # nombrado, cualquier orden
describe_player("María", level=5, score=87)            # mezcla: posicional primero
JunoArgumentos nombrados Nombra los argumentos en la llamada (score=87) y la llamada se explica a sí misma, además puedes pasarlos en cualquier orden. La única regla: cualquier argumento posicional viene antes de los nombrados. Excelente para funciones con un montón de parámetros.
JunoArgumentos nombrados Nombrar argumentos hace una llamada autodocumentada e independiente del orden, con los posicionales requeridos primero. Para firmas amplias o banderas booleanas, prefiere argumentos nombrados para que nadie tenga que contar posiciones para leer la llamada.
JunoArgumentos nombrados Los argumentos nombrados se vinculan por nombre y deben seguir a los posicionales. La palanca real es tu propia firma: un `*` desnudo obliga a los llamadores a nombrar lo que viene después, así que la llamada sigue viéndose bien después de que alguien inserte un parámetro nuevo en el medio.

Valores de retorno

return envía un valor de vuelta al llamador. Sin return, una función devuelve None. Una vez que return se ejecuta, la función sale inmediatamente. Cualquier código después de él en ese bloque es saltado.

return sale de la función y pasa un valor al llamador. Una función sin un return explícito implícitamente devuelve None. return puede aparecer en cualquier lugar en el cuerpo de la función y puede ser usado múltiples veces; el primero alcanzado finaliza la función. Esto hace que los retornos tempranos sean útiles para cláusulas de guarda.

return entrega un valor al llamador y finaliza la función en el acto, y una función que se ejecuta hasta el final sin return devuelve None. Múltiples retornos son buen estilo, no una mala práctica: un return temprano para el fallo o caso borde en la parte superior (el patrón de "cláusula de guarda") se lee mucho mejor que anidar todo el cuerpo dentro de un gran if. Una sutileza a saber antes de que te sorprenda: un return dentro de un bloque try aún ejecuta el bloque finally coincidente primero, así que la limpieza en finally ocurre incluso cuando retornas temprano.

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

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

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

JunoValores de retornoreturn entrega un valor de vuelta a quien llamó la función, y la función se detiene justo allí, cualquier cosa después de él es saltada. Omite return y obtienes None de vuelta. print muestra un valor, return lo devuelve para usar, me tomó un tiempo sentir esa diferencia.
JunoValores de retornoreturn pasa un valor de vuelta y sale inmediatamente, sin return significa None. Puedes tener varios, y un return temprano para el caso borde en la parte superior usualmente se lee más limpio que envolver todo en un gran `if`.
JunoValores de retornoreturn sale en el acto y caer del final da None. Apóyate en retornos tempranos como cláusulas de guarda en lugar de anidamiento profundo. Y recuerda que un return dentro de try aún ejecuta el bloque finally, así que la limpieza allí se dispara incluso en una salida temprana.

Devolver múltiples valores

Python te permite devolver múltiples valores separándolos con comas. El llamador los recibe como una tupla y puede desempaquetarlos en nombres separados en una línea.

Devolver múltiples valores con una coma los empaqueta en una tupla. El llamador desempaqueta con nombres coincidentes. Esto es Pythón idiomático para funciones que naturalmente producen más de un resultado. No es una característica especial; es empaquetamiento y desempaquetamiento de tuplas.

return a, b empaqueta los valores en una tupla, y el llamador los extrae con low, high = f(). Se lee limpiamente para dos o tres resultados. Más allá de eso, la posición se convierte en una responsabilidad: nadie que llama la función recuerda si el tercer elemento era el conteo o el promedio, y un par intercambiado es un error silencioso. En ese punto devuelve algo nombrado en su lugar, un NamedTuple o un @dataclass, así que los llamadores llegan a result.average en lugar de result[2]. Anota el retorno como tuple[int, str] de cualquier forma así un verificador de tipos puede atrapar un llamador desempaquetando el número equivocado 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 desempaquetamiento: Python asigna cada valor devuelto al nombre correspondiente.

JunoDevolver múltiples valores Separa valores con una coma y la función los devuelve juntos como una tupla. El llamador los atrapa en una línea, low, high = min_max(...), un nombre por valor. Práctico siempre que una función naturalmente produce más de una respuesta.
JunoDevolver múltiples valores Una coma empaqueta los valores en una tupla, el llamador desempaqueta con nombres coincidentes. Es empaquetamiento y desempaquetamiento de tuplas simple, no una característica especial. Limpio para dos o tres resultados, menos para más.
JunoDevolver múltiples 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 devuelve un `NamedTuple` o `@dataclass` y deja que los llamadores usen result.average en lugar de contar índices.

Alcance

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 allí y desaparece cuando la función retorna. Un nombre definido en el nivel superior del archivo es global. Leer un global desde dentro de una función funciona sin ceremonia, pero asignarlo necesita global name, de otra forma Python trata la asignación como creando un nuevo local que ensombrece el global. En la práctica casi nunca quieres global: pasar valores como argumentos y devolver resultados con return mantiene los efectos de una función visibles en el sitio de la llamada.

Python resuelve un nombre con la regla LEGB, verificando cuatro lugares en orden: Local (esta función), Enclosing (una función externa que envuelve esta), Global (el módulo), luego Built-in (nombres como len). Leer camina esa cadena; asignación siempre crea un local a menos que digas lo contrario. Esa asimetría es la fuente del bug: count += 1 contra un global sin una línea global count lanza UnboundLocalError, porque la asignación marca count como local para toda la función y la lectura ocurre antes de que exista cualquier valor local. global name opta una asignación de vuelta al nivel del módulo; nonlocal name enfoca la función envolvente más cercana en su lugar, que es cómo un cierre actualiza una variable de la función que la hizo. Trata ambas como una mala señal: una función que llega afuera para mutar estado externo es más difícil de probar que una que toma entradas y devuelve salidas.

JunoAlcance Una variable hecha dentro de una función vive solo allí, el exterior no puede verla. Puedes leer una variable exterior desde adentro, pero cambiar una necesita una línea global primero. Recurre a eso casi nunca: pasa valores adentro, devuelve resultados afuera, y la función permanece simple de seguir.
JunoAlcance Los nombres hechos dentro de una función son locales y desaparecen cuando retorna. Leer un global es gratis, escribir uno necesita global name o Python hace un local que lo ensombrece. Prefiere argumentos adentro y return afuera así los efectos de una función permanecen visibles en la llamada.
JunoAlcance LEGB para búsquedas, pero la asignación siempre hace un local a menos que declares global o nonlocal, que es por qué count += 1 en un global lanza UnboundLocalError. Ambas palabras clave son una mala señal: una función que muta 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 el global
    count += 1

increment()
print(count)   # 1

Usar global debe ser un último recurso. Hace el código más difícil de razonar. Prefiere pasar valores adentro y devolverlos afuera. El alcance se construye directamente sobre cómo la asignación vincula un nombre a un valor, cubierto en el capítulo Variables y tipos.

*args y **kwargs

A veces no sabes cuántos argumentos recibirá una función. *args recoge cualquier número de argumentos posicionales en una tupla. **kwargs recoge cualquier número de argumentos nombrados en un diccionario. Los nombres args y kwargs son convenciones; los asteriscos son lo que importa.

*args recoge los argumentos posicionales excedentes en una tupla. **kwargs recoge los argumentos nombrados excedentes en un dict. Ambos pueden combinarse con parámetros regulares. Los parámetros regulares vienen primero, luego *args, luego parámetros solo nombrados, luego **kwargs. Son útiles para funciones envolventes que pasan argumentos a otra función.

*args reúne los argumentos posicionales sobrantes en una tuple, **kwargs reúne los nombrados sobrantes en un dict. La imagen espejo ocurre en el sitio de la llamada: func(*some_list) esparce una lista en argumentos posicionales y func(**some_dict) esparce un dict en nombrados. Esa simetría es lo que hace funcionar un envolvente de paso directo, def timed(*args, **kwargs): return inner(*args, **kwargs) reenvía lo que sea que obtuvo sin nombrar un solo parámetro. Útil, pero cuesta la firma legible: un envolvente que se traga todo en *args, **kwargs muestra a los llamadores y verificadores de tipos nada sobre qué acepta, así que recurre a él cuando realmente necesites reenviar argumentos arbitrarios, no como la forma predeterminada para una función ordinaria.

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="María", score=87, level=5)

Puedes mezclarlos con parámetros regulares. Los parámetros regulares vienen primero:

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

describe("Intro a Python", "principiante", "python", author="María", year=2024)
Juno*args y **kwargs*args recoge cualquier número de argumentos posicionales extra como una tupla, **kwargs recoge los nombrados extra como un dict. Las palabras args y kwargs son solo convención, los * y ** hacen el trabajo real. Práctico cuando no puedes decir de antemano cuántas cosas llegan.
Juno*args y **kwargs*args recoge argumentos posicionales extra en una tupla, **kwargs los nombrados extra en un dict. Orden en la firma: parámetros regulares, luego *args, luego solo nombrados, luego **kwargs. Más útil para envolventes que reenvían argumentos directamente a otra función.
Juno*args y **kwargs* y ** recogen en una firma y esparcen en un sitio de llamada, que es lo que permite a un envolvente reenviar todo con inner(*args, **kwargs). El costo es una firma que dice a los llamadores nada, así que mantenlo para envolvedores reales, no funciones ordinarias.

Docstrings

Un docstring es una cadena en la parte superior 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 de función. Usa comillas triples, y escribe una línea para funciones simples.

Un docstring es la primera cosa en el cuerpo de la función, una cadena en comillas triples. Las herramientas la leen: help(func) la imprime, y tu editor la muestra cuando pasas el cursor sobre una llamada, así que vale la pena exactamente cuando has olvidado qué hace la función. La convención es una línea de resumen, luego una línea en blanco y más detalle si es necesario. Trátalo como requerido en cualquier cosa llamada desde más de un lugar, opcional en un ayudante desechable cuyo nombre ya dice todo.

Un docstring es la primera sentencia en el cuerpo de una función, clase, o módulo, y debe decir qué hace la función y qué devuelve, no reafirmar la lista de parámetros que un lector ya puede ver. Gana su merecido en cualquier cosa cuyo contrato no sea visible desde la firma: los casos borde, qué lanza, qué hace una entrada vacía. Salta el bloque formal argumento-por-argumento cuando las anotaciones de tipo en la firma ya portan los tipos, ese detalle pertenece en las anotaciones, no duplicado en prosa que cae fuera de fecha. El objetivo es la línea que querrías leer a las 2am mientras debugueas una llamada que escribiste hace seis meses.

python
def normalise(value, min_val, max_val):
    """Escala un valor al rango 0-1 dados el mín y máx conocidos."""
    return (value - min_val) / (max_val - min_val)
python
def build_url(base, version, resource, *, secure=True):
    """
    Construye una URL de endpoint de API.

    Devuelve una cadena de URL completamente calificada. Si secure es False,
    la URL usará http en lugar de 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 claro desde su nombre y firma.

JunoDocstrings Un docstring es una cadena en comillas triples en la muy parte superior de una función, describiendo qué hace. Tu editor la muestra cuando pasas el cursor sobre la función después, que es cuando estarás feliz de que esté allí. Escribe uno siempre que el nombre y las entradas no ya hagan el propósito claro.
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 funciones, añade detalle debajo solo cuando el comportamiento lo necesita. Requerido en cualquier cosa llamada desde varios lugares, omitible en un liner cuyo nombre lo dice todo.
JunoDocstrings Di qué hace la función, qué devuelve, y los casos borde, no una lista de parámetros reafirmada que el lector ya puede ver. Deja que las anotaciones de tipo lleven los tipos así no estés manteniendo los mismos hechos dos veces. Apunta a la línea que querrías a las 2am debugueando una llamada que escribiste hace medio año.

Anotaciones de tipo

Las anotaciones de tipo te permiten anotar qué tipos espera una función y retorna. Python no las impone en tiempo de ejecución, pero los editores las usan para atrapar errores antes de ejecutar nada. El -> antes de los dos puntos especifica el tipo de retorno.

Las anotaciones de tipo son documentación que las herramientas verifican. Los editores y verificadores de tipos (mypy, pyright) las usan para atrapar desajustes de tipo antes del tiempo de ejecución. No tienen 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+).

Las anotaciones de tipo no cambian nada en tiempo de ejecución, Python se ejecuta idénticamente con ellas o sin ellas. Su valor entero es la capa afuera del programa: un verificador de tipos (mypy o pyright) leyendo el código antes de que se ejecute, y tu editor marcando una llamada mala mientras la escribes. Así que una anotación que miente es peor que ninguna, porque el verificador confía en ella. Usa list[int], dict[str, int] y str | None directamente (sin importación de typing necesaria en Python moderno), y anota una función que devuelve nada como -> None, que también señala que llamarla en una expresión (x = log(...)) es un error. El pago llega en funciones llamadas desde muchos lugares: el verificador atrapa un llamador pasando el tipo equivocado en lugar de eso surfacing como un fallo confuso tres marcos de profundidad 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]

Las anotaciones de tipo son opcionales pero valiosas en cualquier función que sea llamada desde múltiples lugares. Son documentación que las herramientas pueden verificar.

JunoAnotaciones de tipo Las anotaciones de tipo notan qué una función toma y devuelve, como name: str y -> str para el retorno. Python no las impondrá cuando se ejecute, pero tu editor las lee y te advierte antes de presionar ejecutar. Piénsalas como notas que las herramientas pueden verificar.
JunoAnotaciones de tipo Las anotaciones son documentación que un verificador de tipos 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 una importación de `typing` en Python moderno. Vale la pena en cualquier cosa llamada desde varios lugares.
JunoAnotaciones de tipo Las anotaciones no hacen nada en tiempo de ejecución, su valor es el verificador y editor leyéndolas, así que una anotación que miente es peor que ninguna. Usa `str | None` y `list[int]` directamente, `-> None` para sin retorno. La victoria es atrapar un llamador con tipo equivocado antes de que falle tres marcos de profundidad.

Funciones como valores

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

Las funciones son objetos de primera clase: tienen un tipo (function), pueden almacenarse en variables y colecciones, y pueden pasarse como argumentos o devolverse como valores. Esta es la fundación de 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 entregar una a otra función es barato. El patrón que crece de esto es el cierre: una función definida dentro de otra que aún alcanza las variables de la función externa después de que la externa ha retornado. Es cómo construyes una función configurada sobre la marcha, una make_multiplier(3) que devuelve una función multiplicando por 3, con el 3 capturado y sostenido. Dos advertencias en código real: un cierre captura la variable, no su valor en el momento de captura, que atrapa a la gente construyendo cierres en un bucle (todos ven el valor final del bucle); y apilar muchas funciones devueltas hace caminos de llamadas difíciles de rastrear, así que usa cierres donde se leen claramente y recurre a una clase cuando el estado crece.

python
def double(x):
    return x * 2

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

apply(double, 5)   # 10

Pasar funciones como argumentos se muestra constantemente con sorted(), map(), y filter(). También lo verás 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 almacenarlo en una variable y entregarlo a otra función. Eso es exactamente lo que permite pasar una función como key= a sorted(). Se sintió extraño para mí al principio, luego hizo clic y apareció en todas partes.
JunoFunciones como valores Las funciones son objetos de primera clase: asígnalas, almacénalas en una lista, pásalas, devuélvelas. Ese es el motor detrás de sorted(key=...), map() y filter(), y detrás de cada devolución de llamada que cablearás después.
JunoFunciones como valores Pasar una función es una referencia, no una copia, y un cierre permite a una función interna seguir alcanzando las variables de la externa después de que retorna. Captura la variable, no el valor, así que construir cierres en un bucle muerde cada vez. Cuando el estado capturado crece, una clase se lee mejor.

En la práctica

Dos funciones que trabajan juntas: letter_grade convierte una puntuación a una letra, y summarise la llama para cada puntuación 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 registro y un procesador de archivo que lo usa, con un parámetro predeterminado dry_run que previene efectos secundarios a menos que sea explícitamente deshabilitado:

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 valor único y un normalizador de columna construido en la parte superior, con anotaciones de tipo y un docstring. La función de columna computa el rango una vez y reutiliza la función escalar para cada elemento:

python
def normalise(value: float, min_val: float, max_val: float) -> float:
    """Escala value al rango 0-1 dados mín y máx conocidos."""
    if max_val == min_val:
        return 0.0
    return (value - min_val) / (max_val - min_val)

def normalise_column(values: list[float]) -> list[float]:
    """Normaliza una columna entera de valores."""
    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))

Las anotaciones de tipo aquí sirven dos propósitos: documentan qué la función espera, y permiten a un verificador de tipos atrapar llamadores que pasan una lista de cadenas por error.