Qué son los type hints y para qué sirven
Los type hints son anotaciones que indican el tipo esperado de variables, parámetros de función y valores de retorno. Fueron introducidas de forma progresiva desde PEP 484 (Python 3.5) y hoy son una práctica estándar en cualquier proyecto profesional.
Python sigue siendo un lenguaje de tipado dinámico: las anotaciones no se validan en tiempo de ejecución. El intérprete las almacena como metadatos, pero no comprueba si los valores cumplen los tipos. Su valor está en:
- Autocompletado y navegación precisos en el editor.
- Detección temprana de errores con verificadores estáticos como
mypyopyright. - Documentación viva: leer una firma de función tipada es mucho más claro que una sin anotar.
- Generación automática de esquemas (Pydantic, FastAPI) y validación en los bordes del programa.
def saludar(nombre: str, edad: int) -> str:
return f"Hola {nombre}, tienes {edad} años"
La firma (nombre: str, edad: int) -> str comunica intención sin ambigüedad.
Los type hints no se ejecutan. Si llamas
saludar(42, "Ana"), Python no se queja: concatenará mal y romperá más adelante. Por eso necesitas un verificador estático (mypy,pyright) que los analice antes de ejecutar el programa.
Anotaciones básicas
Los tipos primitivos se usan con su nombre tal cual:
nombre: str = "Ana"
edad: int = 30
precio: float = 19.99
activo: bool = True
Para funciones, anotar parámetros y retorno:
def suma(a: int, b: int) -> int:
return a + b
def es_mayor(edad: int) -> bool:
return edad >= 18
def saludar() -> None:
print("hola") # None indica que no retorna valor
Parámetros con valor por defecto también se anotan:
def conectar(host: str = "localhost", puerto: int = 8080) -> None:
...
Colecciones genéricas
Desde Python 3.9 se pueden usar los tipos builtin directamente como genéricos, sin importar de typing:
nombres: list[str] = ["Ana", "Bob"]
edades: dict[str, int] = {"Ana": 30, "Bob": 25}
coordenadas: tuple[float, float] = (10.5, 20.3)
ids: set[int] = {1, 2, 3}
La forma antigua List, Dict, Tuple, Set importada desde typing está deprecada en favor de los tipos nativos. Solo persiste por compatibilidad con código antiguo.
Para colecciones homogéneas con tipos específicos:
def sumar_todos(numeros: list[int]) -> int:
return sum(numeros)
def primera_clave(d: dict[str, int]) -> str:
return next(iter(d))
Para una tupla de tamaño variable con el mismo tipo se usa ...:
def producto(numeros: tuple[int, ...]) -> int:
p = 1
for n in numeros:
p *= n
return p
Uniones y opcionales
Un valor que puede ser de varios tipos se declara con |, sintaxis introducida en Python 3.10:
def stringify(x: int | float | str) -> str:
return str(x)
Antes era necesario Union[int, float, str] desde typing. La forma con | es la recomendada hoy.
Para valores que pueden ser None, se usa X | None:
def buscar_usuario(id: int) -> Usuario | None:
...
def mostrar(nombre: str | None = None) -> None:
print(nombre if nombre else "anónimo")
La forma antigua Optional[X] sigue siendo equivalente a X | None. Hoy se prefiere la sintaxis con | por consistencia y por ser más corta.
Alias de tipos
Cuando un tipo se repite en varios sitios, se define un alias:
from typing import TypeAlias
Coordenada: TypeAlias = tuple[float, float]
MatrizCoordenadas: TypeAlias = list[list[Coordenada]]
def distancia(a: Coordenada, b: Coordenada) -> float:
...
Desde Python 3.12 existe una sintaxis nativa más limpia con la palabra clave type:
type Coordenada = tuple[float, float]
type MatrizCoordenadas = list[list[Coordenada]]
Los aliases hacen el código mucho más legible cuando se trabaja con estructuras compuestas.
Funciones tipadas avanzadas
Una función que recibe otra función como parámetro usa Callable:
from collections.abc import Callable
def repetir(accion: Callable[[str], None], texto: str, veces: int) -> None:
for _ in range(veces):
accion(texto)
repetir(print, "hola", 3)
Callable[[argumentos], retorno] describe la firma esperada.
Para genéricos (funciones que trabajan con cualquier tipo manteniendo consistencia):
def primero[T](items: list[T]) -> T:
return items[0]
primero([1, 2, 3]) # T inferido como int
primero(["a", "b", "c"]) # T inferido como str
Esta sintaxis nativa llegó en Python 3.12 con PEP 695. Antes había que declarar los TypeVar a mano, algo más verboso.
TypedDict: tipar diccionarios con estructura fija
Un dict con claves específicas se puede tipar con TypedDict:
from typing import TypedDict
class Usuario(TypedDict):
id: int
nombre: str
email: str
activo: bool
def crear(u: Usuario) -> None:
...
crear({"id": 1, "nombre": "Ana", "email": "ana@ejemplo.com", "activo": True})
El verificador comprobará que las claves existan y que los tipos encajen. Es muy útil para tipar respuestas JSON o estructuras de configuración sin pasar a una clase completa.
Para claves opcionales, se usa total=False o marcadores individuales:
class Usuario(TypedDict):
id: int
nombre: str
email: str
telefono: NotRequired[str] # Python 3.11+
Protocol: tipado estructural (duck typing tipado)
Un Protocol describe qué puede hacer un objeto, no qué clase es. Formaliza el duck typing de Python.
from typing import Protocol
class Cerrable(Protocol):
def close(self) -> None: ...
def limpiar(recurso: Cerrable) -> None:
recurso.close()
Cualquier objeto que tenga un método close() -> None encaja con Cerrable, aunque no herede de él. Ideal para tipar dependencias sin forzar una jerarquía de clases.
Protocolos útiles del estándar:
from typing import Iterable, Iterator, Sized, Hashable
def contar(xs: Iterable[int]) -> int:
return sum(1 for _ in xs)
Iterable, Iterator, Sized, Hashable, Callable están definidos en collections.abc como protocolos, usables como tipos.
Verificación con mypy y pyright
Los type hints no se comprueban en ejecución. Para detectar errores antes de desplegar, se usa un verificador estático.
mypy es el verificador original y el más usado. Se instala como dev dependency:
uv add --dev mypy
Y se ejecuta sobre el proyecto:
uv run mypy src/
Configuración habitual en pyproject.toml:
[tool.mypy]
python_version = "3.13"
strict = true
ignore_missing_imports = true
[[tool.mypy.overrides]]
module = "libreria_sin_stubs.*"
ignore_errors = true
strict = true activa todas las comprobaciones (parámetros sin tipar, retornos implícitos, etcétera). Es lo habitual en código nuevo.
pyright (escrito en TypeScript por Microsoft) es más rápido y suele ser el motor que usan los editores (Pylance en VS Code, también integrado con IntelliJ). Conviene tener ambos: pyright para feedback inmediato en el editor y mypy en CI para una comprobación formal.
Buenas prácticas
Empieza simple: anota las funciones públicas y los tipos en los bordes del programa (entradas de API, salidas a disco). No hace falta tipar cada variable local.
Evita Any: el tipo Any desactiva la verificación. Úsalo solo cuando interactúas con librerías no tipadas y documenta por qué.
Prefiere genéricos concretos: list[Producto] comunica mucho más que list. Si no sabes el tipo, probablemente Iterable[Producto] o un protocolo describa mejor la intención.
Aprovecha los aliases: si una estructura se repite, dale nombre con type Alias = .... El código se vuelve mucho más legible.
CI obligatorio: añade mypy al pipeline para que los PRs que introducen errores de tipo no pasen. Es una red de seguridad que detiene clases enteras de bugs antes de producción.
Los type hints son hoy parte del estándar profesional de Python. Pydantic, FastAPI, SQLModel, Polars y muchas librerías modernas basan gran parte de su API en ellos, y usar un proyecto serio sin anotaciones se considera ya una mala práctica.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Python es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Python
Explora más contenido relacionado con Python y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Añadir anotaciones de tipo a variables, parámetros y valores de retorno. Usar tipos genéricos modernos (list, dict, tuple) directamente sin importar de typing. Declarar uniones con X. Y y opcionales con X. None. Usar TypedDict, Protocol y TypeAlias para casos avanzados. Verificar tipos con mypy o pyright.