Python lleva décadas siendo el lenguaje favorito de gente que no quiere pelearse con el compilador. Escribes, ejecutas, ves el resultado. Sencillo. Pero esa misma flexibilidad tiene un precio: los errores de tipo aparecen en producción, no al guardar el archivo.
Los type hints no cambian eso a nivel de runtime (salvo que uses Pydantic u otras librerías de validación). Python sigue siendo dinámico. Lo que cambia es que ahora tienes herramientas mypy, pyright que analizan el código antes de ejecutarlo y te dicen dónde hay un problema. Es la diferencia entre descubrir que pasas un None donde esperabas un str en tu máquina o en el servidor de tu cliente.
Hay tres ventajas concretas que justifican el esfuerzo:
- El IDE autocompleta con más precisión y señala errores al vuelo.
- El código se documenta solo: la firma de una función anotada dice exactamente qué entra y qué sale.
- Los refactors son más seguros. Si cambias el tipo de retorno de una función, el type checker te dice en qué sitios se rompe.
No hace falta anotarlo todo de golpe. Puedes empezar solo por las funciones públicas de los módulos más importantes y avanzar desde ahí.
Sintaxis básica: funciones, variables y colecciones
La sintaxis de las anotaciones de tipo es sencilla. Dos puntos después del nombre del parámetro, flecha para el retorno:
def saludar(nombre: str) -> str:
return f"Hola, {nombre}"
Para variables sueltas:
contador: int = 0
activo: bool = True
precio: float = 9.99
Con colecciones, desde Python 3.9 ya no necesitas importar nada del módulo typing. Puedes usar las clases built-in directamente como genéricos:
lista: list[str] = []
diccionario: dict[str, int] = {}
conjunto: set[int] = set()
tupla: tuple[str, int] = ("python", 3)
Antes de 3.9 había que escribir List[str], Dict[str, int], etc., importados desde typing. Si tu proyecto tiene que ser compatible con versiones antiguas, añade from __future__ import annotations al principio del archivo y podrás usar la sintaxis nueva igualmente.
Optional y el operador |
Una de las fuentes más frecuentes de AttributeError en Python es tratar un None como si fuera un objeto. Con tipos puedes evitarlo de forma explícita.
Optional[str] significa «puede ser str o puede ser None». Desde Python 3.10 hay una sintaxis más limpia:
# Antes (sigue funcionando)
from typing import Optional
def buscar_usuario(id: int) -> Optional[Usuario]:
...
# Desde Python 3.10
def buscar_usuario(id: int) -> Usuario | None:
...
El operador | no solo funciona con None. Puedes combinar cualquier tipo:
def procesar(valor: int | str | float) -> str:
return str(valor)
El type checker entiende los narrowing guards. Si haces if usuario is not None:, dentro del bloque sabe que usuario no puede ser None y no te avisa si accedes a sus atributos.
Generics y TypeVar
A veces quieres escribir una función que funcione con cualquier tipo, pero que el tipo de entrada y salida estén relacionados. Para eso están los genéricos.
from typing import TypeVar
T = TypeVar('T')
def primero(lista: list[T]) -> T:
return lista[0]
Si llamas a primero([1, 2, 3]), el type checker deduce que el retorno es int. Si llamas a primero(["a", "b"]), deduce que es str. Sin necesidad de sobrecargas ni castings.
Python 3.12 introduce una sintaxis nueva que elimina el TypeVar explícito:
def primero[T](lista: list[T]) -> T:
return lista[0]
Más limpio y más fácil de leer. Si tu proyecto ya requiere 3.12, úsala sin dudarlo.
Protocols: duck typing con verificación estática
Python siempre ha funcionado con duck typing: si un objeto tiene el método que necesitas, da igual de qué clase sea. Los Protocol llevan ese principio al mundo del tipado estático.
from typing import Protocol
class Serializable(Protocol):
def to_dict(self) -> dict:
...
Ahora puedes anotar una función que acepte cualquier objeto que tenga to_dict(), sin importar su herencia:
def guardar(obj: Serializable) -> None:
datos = obj.to_dict()
# ...
Cualquier clase que implemente to_dict() -> dict cumple el protocolo automáticamente. No hay que heredar de Serializable, no hay que registrarla en ningún sitio. El type checker lo verifica en tiempo de análisis.
Si necesitas que funcione también con isinstance() en runtime, añade el decorador @runtime_checkable:
from typing import Protocol, runtime_checkable
@runtime_checkable
class Serializable(Protocol):
def to_dict(self) -> dict:
...
Dataclasses con tipos: la combinación más habitual
Las dataclass y los type hints están hechos el uno para el otro. Las anotaciones de tipo se convierten en la definición de los campos:
from dataclasses import dataclass
@dataclass
class Usuario:
nombre: str
email: str
activo: bool = True
edad: int | None = None
Python genera automáticamente __init__, __repr__ y __eq__. El type checker sabe qué tipo tiene cada campo y avisa si intentas asignar algo incompatible.
Con Pydantic v2 el salto es doble: además del type checking estático, tienes validación en runtime. Si alguien pasa un "hola" donde esperas un int, Pydantic lanza un ValidationError en lugar de dejarlo pasar:
from pydantic import BaseModel
class Usuario(BaseModel):
nombre: str
email: str
edad: int
Para APIs y servicios donde los datos vienen de fuera formularios, JSON, variables de entorno Pydantic es casi imprescindible.
mypy o pyright: cuál elegir en 2026
Ambos analizan el mismo código, pero tienen filosofías distintas.
mypy
Es el type checker original, escrito en Python. Más conservador: cuando no tiene información suficiente, tiende a no avisar en lugar de asumir un error. Útil si tu equipo prefiere menos falsos positivos aunque signifique perderse algún error real. Más lento en proyectos grandes, pero muy configurable mediante mypy.ini o la sección [mypy] en pyproject.toml.
pyright y basedpyright
Pyright está escrito en TypeScript y corre sobre Node. Es notablemente más rápido que mypy en proyectos grandes y más estricto por defecto. Es el motor que usa Pylance en VS Code, así que si tu equipo trabaja en VS Code ya lo tiene integrado sin instalar nada extra.
Basedpyright es un fork de pyright con algunas reglas adicionales y mejoras de ergonomía. Para proyectos nuevos donde el equipo quiere el máximo rigor, es una buena opción.
Para activar el modo estricto en pyright, crea un pyrightconfig.json en la raíz del proyecto:
{
"strict": ["**/*.py"]
}
En modo estricto, pyright exige que todo tenga anotaciones de tipo y avisa de cualquier uso de Any implícito. Puede parecer exagerado al principio, pero en proyectos medianos o grandes ahorra tiempo.
Si ya tienes un proyecto con mypy funcionando, no hay ninguna razón urgente para migrar. Si empiezas desde cero, pyright o basedpyright es la opción más cómoda hoy.
Estrategia para adoptar tipos gradualmente
El mayor error es intentar anotar todo el código de golpe. Lleva horas, genera decenas de errores y el equipo se desanima. Hay una forma más sensata.
Empieza por las funciones públicas. Las que más se llaman desde otros módulos son las más valiosas para anotar. Ahí es donde los errores de tipo tienen más impacto.
Usa # type: ignore con cabeza. Para silenciar errores en código heredado que no vas a tocar ahora. No es trampa, es pragmatismo. Añade un comentario explicando por qué lo ignoras si el motivo no es obvio.
resultado = libreria_sin_tipos.funcion() # type: ignore[no-untyped-call]
Añade py.typed si publicas una librería. Es un archivo vacío en la raíz del paquete que indica a las herramientas que tu librería tiene tipos (PEP 561). Sin él, mypy y pyright ignoran las anotaciones al instalar el paquete como dependencia.
Integra el type checker en CI. Ruff para el linting y pyright para el tipado, los dos en el mismo pipeline. Así los errores no llegan al main.
# En tu workflow de GitHub Actions o similar
- run: pip install pyright ruff
- run: ruff check .
- run: pyright
No necesitas llegar al 100 % de cobertura de tipos para que merezca la pena. Con anotar las funciones más importantes ya reduces los errores más frecuentes. El resto puede esperar.
Si trabajas con herramientas de línea de comandos en Python, los type hints también ayudan en ese contexto: tipado en herramientas CLI Python: seguridad sin overhead. Y si buscas cómo combinar tipos con depuración, mira cómo detectar errores antes de que ocurran: Python tipado y la depuración: encontrar errores antes de que ocurran.
Imagen: Pexels / Daniil Komov
