Tipado en Python: Tipado de funciones

Tabla de Contenido

Cuando un módulo crece y varias funciones empiezan a pasar datos entre capas, el contrato entre parámetros y retorno se vuelve crítico. El tipado de funciones en Python nos permite documentar ese contrato directamente en el código, mejorar la autocompletación del IDE y detectar inconsistencias antes de ejecutar el flujo completo en producción.

Note

Este artículo es parte de una serie que explica el tipado en Python. A continuación, puedes ver la recopilación de todos los posts de la serie.

Ver serie

En la entrega anterior vimos cómo anotar variables y colecciones. Ahora aplicaremos esa misma idea en funciones: parámetros, retornos, valores por defecto, argumentos dinámicos y objetos que se pueden invocar como funciones.

Info

Las anotaciones de tipo no cambian el comportamiento de Python en tiempo de ejecución. Su valor principal está en herramientas de análisis estático como mypy, pyright, IDEs y revisiones de código.

Sintaxis general para tipar funciones

La sintaxis base para agregar tipos a una función consiste en anotar cada argumento después de : y declarar el tipo de retorno después de ->.

def my_function(arg1: type, arg2: type) -> return_type:
    ...

Esta estructura expresa dos cosas importantes:

  • arg1: type: el argumento arg1 debería recibir valores compatibles con type.
  • -> return_type: la función debería devolver un valor compatible con return_type.

Por ejemplo, una función que recibe dos enteros y retorna otro entero se puede escribir así:

def add(a: int, b: int) -> int:
    return a + b

Si la función no retorna un valor útil, se usa None como tipo de retorno:

def log_message(message: str) -> None:
    print(message)

Tip

Anotar el retorno es tan importante como anotar los argumentos. Muchas inconsistencias aparecen cuando una función empieza devolviendo int, luego en una rama retorna None y el consumidor asume que siempre puede operar con números.

Ejemplo con Counter

Supongamos que tenemos una lista de elementos y cada elemento contiene una lista de etiquetas. Queremos contar cuántas veces aparece cada etiqueta.

from collections import Counter


def counter_tags(items: list[dict[str, list[str]]]) -> Counter[str]:
    return Counter(tag for item in items for tag in item["tags"])

La firma de counter_tags nos dice que items debe ser una lista de diccionarios. Cada diccionario usa llaves de tipo str y valores de tipo list[str]. La función retorna un Counter[str], es decir, un contador cuyas llaves son cadenas.

counter_tags([{"tags": ["python", "typing"]}, {"tags": ["python"]}])  # ✅
counter_tags([{"tags": ["python", 1]}, {"tags": ["typing"]}])  # ❌

La segunda llamada es problemática porque la lista de tags mezcla str con int. Python puede ejecutarla, pero un analizador estático marcará que 1 no coincide con list[str].

Warning

El tipado describe la forma esperada de los datos, pero no reemplaza validaciones de entrada cuando los datos vienen de usuarios, APIs externas, archivos o mensajes de una cola.

Valores por defecto tipados

Cuando una función tiene valores por defecto, también conviene anotar esos parámetros. El valor asignado debe ser compatible con el tipo declarado.

def sum_numbers(numbers: list[int], start: int = 0) -> int:
    total = start

    for n in numbers:
        total += n

    return total


sum_numbers([1, 2, 3, 4], start=100)  # ✅
sum_numbers([1, 2, 3, 4])  # ✅
sum_numbers([1, 2, "A"])  # ❌
sum_numbers([1, 2, 3, 4], start="B")  # ❌

En este caso numbers debe ser una lista de enteros y start también debe ser un entero. Si no se pasa start, Python usa 0, que cumple con la anotación int.

Este patrón es útil en funciones acumuladoras, paginadores, transformaciones de datos y cualquier API interna donde un valor opcional modifica el cálculo sin cambiar el tipo esperado.

*args tipados

Python permite recibir una cantidad variable de argumentos posicionales con *args. Para tiparlos, se anota el tipo de cada elemento recibido, no el tipo de la tupla completa.

def sum_args(*args: int) -> int:
    total = 0

    for n in args:
        total += n

    return total


sum_args(1, 2, 3, 4, 5)  # ✅
sum_args(1, "A")  # ❌

La anotación *args: int significa que cada argumento posicional adicional debe ser int. Internamente, Python sigue recibiendo una tupla, pero desde el punto de vista del contrato de la función, cada elemento debe respetar el tipo indicado.

Info

Si necesitas aceptar varios tipos en *args, puedes combinar tipos con |, por ejemplo *args: int | float. Esto mantiene el contrato explícito sin convertir la función en una entrada completamente abierta.

*args con parámetros keyword-only

También podemos combinar argumentos dinámicos con parámetros que solo se reciben por nombre. En Python, todo parámetro declarado después de *args se vuelve keyword-only.

def sum_args(*args: int, start: int = 0) -> int:
    total = start

    for n in args:
        total += n

    return total


sum_args(1, 2, 3, 4)  # ✅
sum_args(1, 2, 3, start=100)  # ✅
sum_args(1, 2, 3, start="x")  # ❌
sum_args(1, 2, "3")  # ❌

Aquí start no puede pasarse como otro argumento posicional más; debe enviarse usando su nombre. Esto mejora la legibilidad cuando el parámetro tiene un rol especial dentro del cálculo.

Este estilo es recomendable cuando una función acepta una secuencia flexible de valores, pero también necesita opciones de configuración con nombres claros.

Funciones de primer orden y Callable

En Python podemos pasar funciones como argumentos de otras funciones. Para anotar este tipo de contrato usamos Callable, disponible en collections.abc.

import time
from collections.abc import Callable


def work(send_notification: Callable[[str], None]) -> None:
    for i in range(3):
        time.sleep(1)

    send_notification(f"Finished {i + 1} rounds of work!")

La anotación Callable[[str], None] indica que send_notification debe ser invocable, recibir un str y no retornar un valor útil.

def notify(message: str) -> None:
    print(message)


def bad_notify(message: int) -> None:
    print(message)


work(notify)  # ✅
work(bad_notify)  # ❌

En Callable, el primer bloque define la lista de argumentos y el segundo define el retorno:

Callable[[str], None]
Callable[[str, int], bool]
Callable[[bytes], str]
flowchart LR
    A["Callable[[str, int], bool]"] --> B[Entrada 1: str]
    A --> C[Entrada 2: int]
    B --> D[Función compatible]
    C --> D
    D --> E[Salida: bool]

Tip

Usa Callable cuando la función consumidora solo necesita ejecutar un comportamiento. Esto reduce acoplamiento porque no obliga a depender de una implementación concreta.

Callable con clases y __call__

Callable no se limita a funciones definidas con def. Una instancia de clase también puede ser invocable si implementa el método especial __call__.

class Notifier:
    def __call__(self, message: str) -> None:
        print(f"Notifier sends: '{message}'")


notifier = Notifier()
work(notifier)  # ✅

En este ejemplo, notifier se comporta como una función porque Notifier define __call__. Para el contrato de work, lo importante es que el objeto pueda invocarse con un str y retornar None.

Este patrón aparece con frecuencia cuando necesitas combinar estado interno con una interfaz funcional. Por ejemplo, un notificador podría guardar credenciales, configuración del canal o métricas, pero seguir exponiendo una firma simple para el consumidor.

Warning

No confundas Callable con una clase específica. Callable[[str], None] describe una capacidad de invocación; no exige que el objeto sea una función tradicional ni una instancia de una clase concreta.

Conclusión

El tipado de funciones en Python permite hacer explícitos los contratos entre piezas de código: qué argumentos acepta una función, qué retorna y qué forma deben tener los callables que recibe como dependencias.

En código de aplicación, estas anotaciones mejoran la navegación, reducen errores de integración y facilitan refactors. En librerías o módulos compartidos, además funcionan como documentación ejecutable para otros desarrolladores y herramientas.

Note

En el siguiente artículo continuaremos con el tipado aplicado a clases, atributos y métodos.

Tipado en clases

Etiquetas :