Type Hinting y Mypy en Python: de las anotaciones básicas al tipado avanzado
El precio del tipado dinámico cuando el proyecto crece
Python es conocido por su flexibilidad y facilidad de uso, en gran parte debido a su naturaleza de tipado dinámico. Esto significa que no necesitas declarar explícitamente el tipo de una variable cuando la creas; Python lo infiere en tiempo de ejecución. Si bien esto acelera el desarrollo inicial, puede convertirse en una fuente de problemas a medida que tus proyectos crecen en tamaño y complejidad, o cuando trabajas en equipo.
Imagina que estás trabajando en una función que espera una lista de números, pero accidentalmente le pasas una cadena de texto. Python no te avisará de este error hasta que la función intente realizar una operación numérica con la cadena, resultando en un error en tiempo de ejecución. Esto puede ser especialmente problemático en código que no se ejecuta con frecuencia o en partes críticas de una aplicación que solo fallan bajo ciertas condiciones.
Además, la falta de información de tipos explícita dificulta la lectura y comprensión del código. ¿Qué tipo de datos espera esta función? ¿Qué tipo de datos devuelve? Sin type hints, a menudo tienes que recurrir a la documentación (si existe y está actualizada), o peor aún, a leer la implementación de la función para entender sus expectativas. Esto ralentiza el desarrollo, hace que la refactorización sea más arriesgada y reduce la eficacia del autocompletado y la verificación de errores de tu IDE.
Aquí es donde el type hinting y herramientas como Mypy entran en juego, ofreciendo una solución elegante para añadir una capa de robustez y claridad a tu código Python sin sacrificar la flexibilidad del lenguaje. Los fundamentos de abajo no han cambiado; lo que sí cambió es hasta dónde llegan: la última sección de este artículo cubre lo que el tipado clásico no resuelve —shapes de tensores, validación en el borde de un sistema, genéricos modernos— y qué checker usar en 2026 cuando mypy deja de ser la única opción razonable.
Conceptos Clave
Tipado Dinámico vs. Estático
- Tipado Dinámico: En Python, el tipo de una variable se determina en tiempo de ejecución. Esto permite una gran flexibilidad, pero los errores de tipo solo se descubren cuando el código se ejecuta.
- Tipado Estático: En lenguajes como Java o C++, los tipos de las variables se declaran explícitamente y se verifican en tiempo de compilación. Esto ayuda a detectar errores antes de que el programa se ejecute, pero puede ser más verboso.
Python, con el type hinting, busca un equilibrio, permitiéndote añadir información de tipo opcional que puede ser verificada estáticamente por herramientas externas.
Type Hinting (PEP 484)
El type hinting es una característica introducida en Python 3.5 (a través de la PEP 484) que permite a los desarrolladores especificar los tipos esperados de argumentos de función, valores de retorno, variables y atributos de clase. Es importante entender que estos hints son solo eso: sugerencias. Python los ignora en tiempo de ejecución, lo que significa que no afectan el comportamiento del programa. Su propósito principal es ser utilizados por herramientas de análisis estático de código, IDEs y otros desarrolladores.
Algunos tipos comunes que usarás:
- Tipos básicos:
int,str,float,bool. - Colecciones:
List[int],Dict[str, float],Tuple[str, int, bool],Set[str]. Necesitas importarlos desde el módulotyping. - Tipos especiales del módulo
typing:Optional[T]: Indica que un valor puede ser de tipoToNone. Es equivalente aUnion[T, None].Union[T1, T2]: Indica que un valor puede ser de tipoT1oT2.Any: Indica que el tipo es desconocido o puede ser cualquier cosa. Úsalo con precaución, ya que anula la verificación de tipos.Callable[[Arg1Type, Arg2Type], ReturnType]: Para funciones o cualquier objeto invocable.TypeVar: Para definir tipos genéricos.TypedDict: Para definir la estructura de diccionarios con claves y tipos específicos.
Mypy
Mypy es un verificador de tipos estático opcional para Python. Su función es leer tu código Python, interpretar los type hints que has añadido y verificar si hay inconsistencias de tipo. Si Mypy encuentra un lugar donde un tipo no coincide con lo que se esperaba (por ejemplo, pasas una cadena a una función que espera un entero), te lo notificará antes de que ejecutes tu código. Piensa en Mypy como un 'linter' para tipos.
Las ventajas de usar Mypy son significativas:
- Detección temprana de errores: Atrapa errores de tipo antes de que lleguen a producción.
- Mejora la legibilidad: El código con type hints es más fácil de entender para otros desarrolladores y para tu yo futuro.
- Facilita la refactorización: Puedes cambiar el tipo de un argumento o retorno y Mypy te ayudará a encontrar todos los lugares afectados.
- Mejor soporte de IDE: Los IDEs modernos utilizan los type hints para ofrecer un autocompletado más preciso y una mejor verificación de errores en tiempo real.
De la sintaxis básica a mypy en la terminal
1. Instalación de Mypy
Lo primero es instalar Mypy en tu entorno virtual. Es una herramienta de desarrollo, por lo que no es una dependencia de tu aplicación en producción.
pip install mypy2. Sintaxis Básica de Type Hints
Veamos cómo aplicar los type hints en diferentes escenarios.
Variables
Puedes anotar variables para indicar su tipo esperado. Esto es útil para la claridad, aunque Mypy a menudo puede inferir el tipo de asignaciones simples.
nombre: str = "Alice"
edad: int = 30
salario: float = 50000.50
es_activo: bool = TrueParámetros de Función y Valores de Retorno
Esta es la aplicación más común y beneficiosa de los type hints.
def saludar(nombre: str) -> str:
return f"Hola, {nombre}!"
def sumar(a: int, b: int) -> int:
return a + b
def dividir(dividendo: float, divisor: float) -> float:
if divisor == 0:
raise ValueError("No se puede dividir por cero")
return dividendo / divisorColecciones (Listas, Diccionarios, Tuplas, Sets)
Para colecciones, necesitas especificar el tipo de los elementos que contienen. Para esto, importamos los tipos genéricos del módulo typing.
from typing import List, Dict, Tuple, Set
productos: List[str] = ["manzana", "pera", "uva"]
precios: Dict[str, float] = {"manzana": 1.2, "pera": 0.8}
coordenadas: Tuple[float, float] = (10.5, 20.3)
usuarios_activos: Set[int] = {101, 105, 203}Optional y Union
Optional[T] se usa cuando un valor puede ser de tipo T o None. Union[T1, T2] se usa cuando un valor puede ser de tipo T1 o T2.
from typing import Optional, Union
def obtener_usuario(id_usuario: int) -> Optional[str]:
if id_usuario == 1:
return "Alice"
return None
def procesar_entrada(valor: Union[str, int]) -> str:
if isinstance(valor, int):
return f"Número recibido: {valor}"
return f"Cadena recibida: {valor.upper()}"Clases Personalizadas
Puedes usar tus propias clases como tipos.
class Producto:
def __init__(self, nombre: str, precio: float):
self.nombre = nombre
self.precio = precio
def mostrar_producto(p: Producto) -> str:
return f"Producto: {p.nombre}, Precio: {p.precio:.2f}"
mi_producto = Producto("Laptop", 1200.50)
print(mostrar_producto(mi_producto))3. Ejecutando Mypy
Una vez que has añadido los type hints a tu código, puedes ejecutar Mypy desde la línea de comandos para verificarlo.
Crea un archivo llamado ejemplo_tipos.py con el siguiente contenido:
from typing import TypedDict
class Pedido(TypedDict):
producto: str
cantidad: int
precio: float
def calcular_total_compra(precios_productos: dict[str, float], cantidades: dict[str, int]) -> float:
total = 0.0
for producto, precio in precios_productos.items():
cantidad = cantidades.get(producto, 0)
total += precio * cantidad
return total
def obtener_nombre_cliente(id_cliente: int) -> str | None:
if id_cliente == 101:
return "Juan Pérez"
elif id_cliente == 102:
return "María García"
return None
def procesar_pedidos(pedidos: list[Pedido]) -> list[str]:
resultados = []
for pedido in pedidos:
resultados.append(
f"Pedido: {pedido['producto']} x {pedido['cantidad']} a {pedido['precio']:.2f} cada uno."
)
return resultados
# Ejemplo de uso correcto
precios = {"manzana": 1.5, "pan": 2.0, "leche": 1.0}
cantidades_compra = {"manzana": 2, "pan": 1}
print(f"Total de la compra: {calcular_total_compra(precios, cantidades_compra):.2f}")
cliente = obtener_nombre_cliente(101)
if cliente:
print(f"Cliente encontrado: {cliente}")
pedidos_ejemplo: list[Pedido] = [
{"producto": "Camisa", "cantidad": 2, "precio": 25.50},
{"producto": "Pantalón", "cantidad": 1, "precio": 40.00},
{"producto": "Zapatos", "cantidad": "uno", "precio": 60.00}, # Error intencional
]
print("\nProcesando pedidos:")
for res in procesar_pedidos(pedidos_ejemplo):
print(res)Ahora, ejecuta Mypy en tu terminal:
mypy ejemplo_tipos.pyEsta es la salida real de ejecutar mypy 2.3.0 sobre ese fichero (verificado hoy con uvx mypy, no una salida de ejemplo inventada):
ejemplo_tipos.py:47: error: Incompatible types (expression has type "str", TypedDict item "cantidad" has type "int") [typeddict-item]
Found 1 error in 1 file (checked 1 source file)El fichero usa deliberadamente TypedDict en vez de Dict[str, Union[str, int, float]]: una unión que cubre los tres tipos posibles de los valores de un pedido no puede, por definición, marcar como error un valor que sí pertenece a esa unión —"uno" es un str válido dentro de Union[str, int, float], así que ese diseño nunca habría detectado nada—. TypedDict es la herramienta correcta aquí porque asigna un tipo distinto a cada clave (producto: str, cantidad: int, precio: float), y es exactamente ese contraste por clave el que le permite a Mypy señalar que "cantidad": "uno" no encaja con cantidad: int, sin tocar el resto del diccionario.
Qué devuelve mypy en la práctica
Este es el resultado real de ejecutar mypy 2.3.0 (versión estable publicada el 13 de julio de 2026, ver PyPI) sobre un fichero con tres errores de tipos introducidos a propósito:
# bad_types.py
def greet(name: str) -> str:
return "Hola " + name
greet(42)
nums: list[int] = [1, 2, 3]
nums.append("4")
def total(items: list[int]) -> int:
return sum(items)
result: str = total(nums)$ mypy bad_types.py
bad_types.py:6: error: Argument 1 to "greet" has incompatible type "int"; expected "str" [arg-type]
bad_types.py:9: error: Argument 1 to "append" of "list" has incompatible type "str"; expected "int" [arg-type]
bad_types.py:16: error: Incompatible types in assignment (expression has type "int", variable has type "str") [assignment]
Found 3 errors in 1 file (checked 1 source file)Cada línea señala el fichero, la línea exacta, el tipo esperado frente al recibido y el código de error entre corchetes ([arg-type], [assignment]). Ese código es lo que te permite silenciar un caso concreto con # type: ignore[arg-type] sin desactivar toda la comprobación.
4. Configuración de Mypy (mypy.ini)
Para proyectos más grandes, querrás configurar Mypy para que se adapte a tus necesidades. Puedes hacerlo creando un archivo mypy.ini en la raíz de tu proyecto.
[mypy]
python_version = 3.12
warn_unused_ignores = True
warn_redundant_casts = True
# Prohíbe funciones sin ningún type hint (superset de disallow_incomplete_defs)
disallow_untyped_defs = True
# Prohíbe funciones con anotaciones parciales (algunos parámetros tipados y otros no);
# las funciones completamente sin anotar ya quedan cubiertas por disallow_untyped_defs
disallow_incomplete_defs = True
# Report errors for missing imports
ignore_missing_imports = False
# Show error codes
show_error_codes = True
[mypy-mi_modulo_externo.*]
# Ignorar un módulo específico que no tiene type hints
ignore_missing_imports = TrueEjecuta Mypy de nuevo, y automáticamente buscará este archivo de configuración.
mypy .Esto verificará todos los archivos Python en el directorio actual y sus subdirectorios.
Un gestor de tareas tipado de principio a fin
Vamos a construir un pequeño gestor de tareas para ilustrar cómo los type hints mejoran la claridad y la robustez.
Crea un archivo task_manager.py:
from typing import List, Dict, Optional, Union, TypedDict
from datetime import datetime
# Definimos un TypedDict para la estructura de una tarea
class Task(TypedDict):
id: int
titulo: str
descripcion: Optional[str]
fecha_creacion: datetime
completada: bool
class TaskManager:
def __init__(self) -> None:
self.tasks: List[Task] = []
self._next_id: int = 1
def add_task(self, titulo: str, descripcion: Optional[str] = None) -> Task:
new_task: Task = {
"id": self._next_id,
"titulo": titulo,
"descripcion": descripcion,
"fecha_creacion": datetime.now(),
"completada": False
}
self.tasks.append(new_task)
self._next_id += 1
return new_task
def get_task(self, task_id: int) -> Optional[Task]:
for task in self.tasks:
if task["id"] == task_id:
return task
return None
def update_task_status(self, task_id: int, completada: bool) -> bool:
task = self.get_task(task_id)
if task:
task["completada"] = completada
return True
return False
def list_tasks(self, solo_pendientes: bool = False) -> List[Task]:
if solo_pendientes:
return [task for task in self.tasks if not task["completada"]]
return self.tasks
def delete_task(self, task_id: int) -> bool:
initial_len = len(self.tasks)
self.tasks = [task for task in self.tasks if task["id"] != task_id]
return len(self.tasks) < initial_len
# --- Mini Aplicación de Prueba ---
if __name__ == "__main__":
manager = TaskManager()
print("Añadiendo tareas...")
manager.add_task("Comprar víveres", "Leche, pan, huevos")
manager.add_task("Estudiar Mypy", "Revisar la documentación oficial")
manager.add_task("Preparar presentación")
print("\nTodas las tareas:")
for task in manager.list_tasks():
print(f"- [{'X' if task['completada'] else ' '}] {task['id']}: {task['titulo']} (Creada: {task['fecha_creacion'].strftime('%Y-%m-%d')})")
print("\nCompletando tarea 1...")
manager.update_task_status(1, True)
print("\nTareas pendientes:")
for task in manager.list_tasks(solo_pendientes=True):
print(f"- [{'X' if task['completada'] else ' '}] {task['id']}: {task['titulo']}")
print("\nIntentando obtener tarea inexistente (ID 99):")
tarea_inexistente = manager.get_task(99)
if tarea_inexistente is None:
print("Tarea no encontrada, como se esperaba.")
print("\nEliminando tarea 2...")
if manager.delete_task(2):
print("Tarea 2 eliminada.")
print("\nTareas restantes:")
for task in manager.list_tasks():
print(f"- [{'X' if task['completada'] else ' '}] {task['id']}: {task['titulo']}")Para ejecutar este mini-proyecto y verificarlo con Mypy:
python task_manager.py
mypy task_manager.pyMypy debería pasar sin errores, demostrando que la estructura de tipos es consistente. Si intentaras, por ejemplo, pasar un entero como título a add_task, Mypy te lo señalaría.
Errores Comunes y Depuración
Aunque el type hinting es una herramienta poderosa, hay algunas trampas comunes que los desarrolladores suelen encontrar:
1. Olvidar importar tipos del módulo typing
Es un error muy común. Si usas List, Dict, Optional, etc., debes importarlos explícitamente.
# Incorrecto
def procesar_items(items: List[str]) -> None:
pass
# Correcto
from typing import List
def procesar_items(items: List[str]) -> None:
pass2. Uso excesivo o incorrecto de Any
Any es el comodín del type hinting. Le dice a Mypy que "confíe en mí, sé lo que estoy haciendo" y desactiva la verificación de tipos para esa parte del código. Si bien es útil para interactuar con código sin anotaciones o para prototipos rápidos, su uso excesivo anula los beneficios del type hinting.
from typing import Any
# Evitar esto si es posible
def procesar_datos_genericos(data: Any) -> Any:
# ... lógica que podría fallar si 'data' no es del tipo esperado
return dataIntenta ser lo más específico posible con tus tipos. Si no estás seguro, Union o Optional suelen ser mejores alternativas que Any.
3. Ignorar los errores de Mypy
Mypy está ahí para ayudarte. Si reporta un error, tómate el tiempo para entender por qué. A menudo, revela un problema real en tu lógica o una ambigüedad en tus tipos.
Si estás seguro de que Mypy está equivocado o si estás lidiando con una limitación conocida, puedes usar # type: ignore en la línea donde Mypy reporta el error. Úsalo con moderación y con un comentario que explique por qué lo estás ignorando.
def sumar_numeros(a: int, b: int) -> int:
return a + b
resultado = sumar_numeros("1", 2) # Mypy reportará un error aquí
resultado_ignorado = sumar_numeros("1", 2) # type: ignore # ¡No hagas esto a menos que sea realmente necesario!4. Problemas con librerías de terceros sin stubs
Algunas librerías antiguas o menos mantenidas pueden no tener type hints o archivos .pyi (archivos de stubs que contienen solo las anotaciones de tipo). Mypy puede quejarse de "missing imports" o de no poder inferir tipos.
Puedes configurar Mypy para ignorar los módulos que no tienen type hints en tu archivo mypy.ini:
[mypy]
ignore_missing_imports = True
[mypy-nombre_de_la_libreria_sin_tipos.*]
ignore_missing_imports = TrueO puedes instalar stubs de terceros si están disponibles (ej: pip install types-requests para la librería requests).
5. Tipos genéricos y TypeVar
Cuando trabajas con funciones o clases que operan sobre diferentes tipos pero mantienen la relación de tipo, TypeVar es esencial. Por ejemplo, una función identity que devuelve exactamente el mismo tipo que recibe.
from typing import TypeVar
T = TypeVar('T')
def identity(arg: T) -> T:
return arg
valor_str: str = identity("hola")
valor_int: int = identity(123)
# Mypy detectaría un error aquí:
# valor_str_error: str = identity(123) # type: ignoreSi no usaras TypeVar y solo pusieras Any, perderías la verificación de que el tipo de retorno es el mismo que el de entrada. Esta sintaxis sigue siendo válida y necesaria en cualquier base de código con Python anterior a 3.12; en proyectos ya en 3.12+ hay una forma más directa de declarar genéricos, que se ve más abajo, en la sección de PEP 695/696.
Cuando el tipo no basta: shapes de arrays y tensores
Todo lo anterior —desde Optional y TypedDict hasta el TypeVar de la sección anterior— cubre el caso general de cualquier programa Python. Pero quien trabaja con arrays de NumPy, tensores de PyTorch o pipelines de ML se topa con un límite que List, Dict o TypeVar no resuelven. La respuesta habitual a "¿cómo tipo un tensor?" sigue siendo NDArray[np.float64] de numpy.typing, y basta para que mypy o pyright dejen de quejarse, pero nunca resuelve el bug que de verdad rompe un pipeline de entrenamiento: pasar un tensor con la forma equivocada. NDArray[np.float64] fija el tipo de dato, no la forma, así que un vector de 256 elementos y un batch de imágenes 32×3×224×224 pasan exactamente el mismo chequeo estático:
import numpy as np
from numpy.typing import NDArray
def normalize(batch: NDArray[np.float64]) -> NDArray[np.float64]:
"""Type-checks con mypy o pyright, pero no garantiza ninguna forma."""
return (batch - batch.mean(axis=0)) / batch.std(axis=0)
# Ambas llamadas superan el chequeo estático exactamente igual,
# aunque una es un vector 1D y la otra un tensor 4D de imágenes.
normalize(np.zeros((256,), dtype=np.float64))
normalize(np.zeros((32, 3, 224, 224), dtype=np.float64))NumPy 2.x sí hizo ndarray genérico sobre la forma además del dtype (np.ndarray[tuple[int, int], np.dtype[np.float64]]), apoyándose en PEP 646 (TypeVarTuple, aceptada para Python 3.11). Pero la documentación oficial de numpy.typing es explícita sobre el límite: ese tuple[int, int] solo fija el número de dimensiones, no su tamaño. No hay forma de declarar con numpy.typing puro "esta matriz es 3×4"; enteros literales en la forma no están soportados.
La librería que se consolidó como estándar de facto para esto es jaxtyping. Pese al nombre, ya no depende de JAX: anota shape y dtype de tensores de PyTorch, NumPy, TensorFlow y MLX con la misma sintaxis. La forma se declara como una cadena de símbolos separados por espacios, reutilizables entre argumentos para expresar relaciones (mismo batch, misma dimensión de embedding) que numpy.typing no puede capturar:
from jaxtyping import Float, jaxtyped
from beartype import beartype
import torch
from torch import Tensor
@jaxtyped(typechecker=beartype)
def scaled_dot_product_attention(
query: Float[Tensor, "batch heads seq dim"],
key: Float[Tensor, "batch heads seq dim"],
value: Float[Tensor, "batch heads seq dim"],
) -> Float[Tensor, "batch heads seq dim"]:
dim = query.shape[-1]
scores = query @ key.transpose(-2, -1) / dim ** 0.5
weights = scores.softmax(dim=-1)
return weights @ valueSi query, key y value no comparten batch, heads, seq o dim, la llamada falla en el momento exacto de la invocación con un error de beartype que señala qué eje no coincide, no tres capas más abajo dentro de un kernel de atención. Tres matices que no suelen aparecer en los ejemplos rápidos: el chequeo en tiempo de ejecución necesita un backend (beartype, o typeguard en su serie 2.x, ya que las versiones 3 y 4 tienen problemas conocidos de compatibilidad con jaxtyping); es incompatible con anotaciones diferidas (from __future__ import annotations o cadenas de tipo), porque el backend necesita evaluar la anotación en tiempo real para comparar símbolos entre argumentos; y ningún type checker estático comprueba estas shapes. La documentación de jaxtyping lo dice sin rodeos: comprobar shape y dtype por completo queda "fuera del alcance de lo que el chequeo estático puede hacer hoy". Para mypy o pyright, Float[Tensor, "batch heads seq dim"] es simplemente Tensor; toda la verificación real ocurre en tiempo de ejecución.
Validar el borde con Pydantic v2
jaxtyping resuelve el interior del pipeline —funciones puras que reciben y devuelven tensores—, pero en el borde (payloads JSON, configuración, datos que llegan de una API) el trabajo lo sigue haciendo Pydantic. El primer ajuste práctico es de vocabulario: el decorador @validator de Pydantic v1 está deprecado. La documentación actual de Pydantic confirma que el decorador vigente es @field_validator, o su equivalente declarativo con Annotated y BeforeValidator/AfterValidator:
from typing import Annotated
import numpy as np
from pydantic import BaseModel, BeforeValidator, ConfigDict
def _as_2d_float_array(value: object) -> np.ndarray:
array = np.asarray(value, dtype=np.float64)
if array.ndim != 2:
raise ValueError(f"se esperaba un tensor 2D, llegó ndim={array.ndim}")
return array
Matrix2D = Annotated[np.ndarray, BeforeValidator(_as_2d_float_array)]
class BatchRequest(BaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True)
features: Matrix2D
labels: Matrix2DPydantic no tiene soporte nativo para np.ndarray (falla con PydanticSchemaGenerationError si lo declaras sin más), así que el patrón real es arbitrary_types_allowed=True combinado con un BeforeValidator que hace la coerción. Cuando lo que se necesita es declarar shape y dtype sin escribir a mano ese validador, la librería especializada numpydantic expone shapes con nombres de eje y comodines: features: NDArray[Shape["* batch, 128 embedding"], float]. La diferencia de fondo con jaxtyping no es de sintaxis sino de dónde se paga el coste: jaxtyping apunta a las funciones de cómputo dentro del bucle de entrenamiento o inferencia, y decorar con jaxtyped y beartype tiene un coste real en tiempo de ejecución que conviene medir antes de activarlo sin condiciones en cada forward pass de producción. Pydantic, en cambio, ya asume que la validación se paga una vez por request o por carga de config. No son alternativas, son capas distintas del mismo pipeline.
Genéricos modernos: PEP 695 y PEP 696
La pieza que cambió de forma más silenciosa es la sintaxis de genéricos que vimos arriba con TypeVar. Python 3.12 aceptó PEP 695: en vez de instanciar TypeVar aparte y heredar de Generic[T], el parámetro de tipo se declara entre corchetes directamente en la clase, función o alias. Python 3.13 sumó PEP 696, que permite darle un valor por defecto:
import torch
class Dataset[T]:
def __init__(self, items: list[T]) -> None:
self._items = items
def sample(self, n: int) -> list[T]:
return self._items[:n]
type Batch[T] = list[T]
class ModelOutput[T = torch.Tensor]:
def __init__(self, logits: T, loss: T | None = None) -> None:
self.logits = logits
self.loss = lossModelOutput se puede seguir usando genérico (ModelOutput[np.ndarray] para un pipeline que ya salió de torch), pero si no se especifica nada, el checker asume torch.Tensor. Es una mejora modesta de ergonomía, pero elimina el boilerplate de TypeVar + Generic que llenaba de ruido cualquier Dataset, DataLoader o wrapper de salida de modelo genérico sobre el framework de turno. En código que todavía soporta versiones de Python anteriores a 3.12, TypeVar sigue siendo la única opción; en proyectos ya en 3.12+, esta sintaxis es la que conviene usar para código nuevo.
El mapa de type checkers en 2026: mypy, pyright, ty y Pyrefly
Hasta hace poco elegir checker era casi automático: mypy por defecto, pyright si el editor era VS Code. Hoy conviene comparar los cuatro por criterios verificables —madurez, si comparten plugins o flags con mypy, y qué tan bien se integran en el editor— antes de decidir, porque no todos compiten en lo mismo:
- mypy sigue siendo el incumbente: estable desde hace más de una década, con su propio sistema de plugins (para Django, attrs y similares) que ninguno de los tres siguientes replica. Su soporte de TypeVarTuple —la base de la tipificación de shapes— llegó tarde y sigue con casos borde sin resolver. No tiene un LSP propio maduro: se integra en el editor vía extensiones de terceros o el modo
dmypyen watch mode. - pyright (Microsoft, open source) tuvo una de las dos implementaciones de referencia citadas en el propio PEP 646 (junto con Pyre) antes de que mypy ofreciera soporte completo. Trae su propio LSP (
pyright-langserver), que funciona directamente en Neovim, Sublime, Emacs o PyCharm sin depender de VS Code; Pylance, la extensión que la mayoría de usuarios de VS Code usa en la práctica, es una capa propietaria de Microsoft construida encima de ese mismo Pyright. No comparte plugins con mypy: es un ecosistema de configuración propio. - ty, de Astral (los creadores de Ruff y uv), sigue en fase beta a fecha de hoy (versionado
0.0.x, sin API estable todavía), con releases semanales y objetivo declarado de versión estable durante 2026. Es también su propio LSP, pensado para re-chequeo rápido en el editor. Su FAQ oficial es explícita sobre dos límites frente a mypy: "no tiene sistema de plugins y de momento no hay planes de añadir uno", y no existe un flag--strictpropio (aunque el proyecto se describe "razonablemente estricto por defecto"). Tampoco avisa de funciones sin anotar como hacedisallow_untyped_defsen mypy: por diseño, las infiere comoUnknowny deja la exigencia de cobertura de tipos a herramientas externas como Ruff. En marzo de 2026 OpenAI anunció la adquisición de Astral para integrar uv, Ruff y ty en Codex; a día de hoy la operación sigue pendiente de cierre regulatorio y ambas compañías operan de forma independiente. - Pyrefly, de Meta, alcanzó la versión estable 1.0 en mayo de 2026. Ya era el checker por defecto para los desarrolladores de Instagram dentro de Meta antes incluso de llegar a la versión 1.0, y PyTorch y NumPy lo han adoptado en producción, mientras que pandas lo usa para revisar sus stubs de tipos. Una instalación nueva sin configuración previa no arranca estricta: usa por defecto el preset
basic, que según el propio anuncio de la versión 1.0 "muestra solo errores de alta confianza, muy probablemente indicadores de bugs reales" (sintaxis, imports que no resuelven) y silencia el resto de diagnósticos hasta que se active explícitamente un preset más estricto. Sin plugins de mypy, trae su propio LSP (su extensión es, según su propio equipo, la más descargada del registro Open VSX).
Lo más relevante para el tema de este artículo: el equipo de Pyrefly presentó en la Typing Summit de PyCon US 2026 una propuesta para llevar las formas de tensores dentro del propio sistema de tipos, con aritmética simbólica sobre las dimensiones en vez de un comentario junto al código. Aquí el límite lo tiene jaxtyping, no Pyrefly: la documentación oficial de Pyrefly señala que con jaxtyping "no hay forma de compartir dimensiones simbólicas entre variables y funciones de una misma clase", lo que impide tipar de punta a punta una jerarquía de módulos conectados entre sí. El sistema nativo de Pyrefly sí lo resuelve —su equipo reporta que un port fiel de NanoGPT completo "no se puede lograr usando solo la sintaxis de jaxtyping"— y además acepta esa sintaxis como front-end alternativo, traduciéndola internamente a sus propios genéricos. El estado de la función, eso sí, sigue siendo experimental: la API puede cambiar sin previo aviso en cualquier release, y sus ejemplos por ahora se limitan a PyTorch.
| Checker | Madurez | Plugins/flags estilo mypy | LSP / editor | Elígelo si... |
|---|---|---|---|---|
| mypy | Estable, más de 10 años | Sistema de plugins propio (Django, attrs, etc.) | Sin LSP propio maduro; CLI o dmypy en watch mode | Ya lo usas sin fricción de rendimiento o dependes de un plugin que las otras tres no replican |
| pyright | Estable desde 2019 | Ecosistema propio, no comparte plugins con mypy | LSP propio (pyright-langserver): funciona fuera de VS Code; Pylance lo envuelve para VS Code | Tu editor ya vive en el ecosistema pyright/Pylance, o quieres LSP maduro fuera de VS Code |
| ty | Beta, versionado 0.0.x, sin API estable | Sin plugins ("no hay planes de añadir uno") ni --strict propio; no marca funciones sin anotar como error | LSP propio, foco explícito en velocidad de re-chequeo en el editor | Priorizas feedback de editor muy rápido y toleras que aún falte cobertura de la spec |
| Pyrefly | Estable 1.0 desde mayo de 2026 | Sin plugins de mypy; instalación nueva arranca en preset basic (solo errores de alta confianza), no estricta por defecto | LSP propio; extensión más descargada de Open VSX según su equipo | El dolor real es velocidad en CI/editor sobre una base de código grande y quieres algo ya estable, no beta |
| Caso | Usa | Nota |
|---|---|---|
| Bugs de shape en funciones torch/numpy/jax | jaxtyping + jaxtyped + beartype | Actívalo en tests y modo debug; mide el coste antes de dejarlo en cada forward pass de producción |
| Validar payloads JSON o config externos | Pydantic v2 (Annotated + BeforeValidator) o numpydantic | numpydantic evita escribir el validador a mano cuando el shape se puede declarar directamente |
| Estructuras genéricas propias (datasets, dataloaders, wrappers) | PEP 695/696: class Dataset[T], [T = torch.Tensor] | Sustituye el boilerplate de TypeVar + Generic en proyectos ya en Python 3.12+ |
| Elegir checker en 2026 | mypy si no hay fricción de rendimiento; pyright si ya vives en su ecosistema o quieres LSP maduro fuera de VS Code; Pyrefly 1.0 si el dolor es velocidad y quieres algo ya estable | ty solo si toleras software en beta; depende además de cómo se resuelva su integración en Codex tras el cierre de la adquisición de Astral |
Automatizar la verificación: CI/CD, TypedDict y stubs de terceros
El tipado avanzado de las secciones anteriores no sustituye tres piezas más mundanas que conviene tener resueltas en cualquier proyecto que ya usa mypy en serio: que la verificación corra sola en cada cambio, que las estructuras de diccionario complejas tengan su propio tipo, y que las librerías de terceros sin anotaciones no bloqueen el chequeo del resto del código.
1. Integración con CI/CD
Automatiza la ejecución de Mypy en tu pipeline de Integración Continua/Despliegue Continuo (CI/CD). Esto asegura que todo el código nuevo o modificado cumpla con los estándares de tipo antes de ser fusionado o desplegado. Herramientas como GitHub Actions, GitLab CI o Jenkins pueden configurarse fácilmente para ejecutar mypy . como parte de tus pruebas.
2. TypedDict para estructuras de diccionario complejas
Ya lo usamos en el mini-proyecto, pero profundiza en TypedDict. Es increíblemente útil para definir la forma de diccionarios que se usan como estructuras de datos, proporcionando verificación de tipos para claves y valores, lo cual es un gran paso adelante de los diccionarios de Python sin tipado.
3. Escribir archivos de stubs (.pyi)
Si trabajas con una librería de terceros que carece de type hints, puedes contribuir a la comunidad escribiendo archivos .pyi. Estos archivos contienen solo las anotaciones de tipo para una librería, permitiendo que Mypy y los IDEs la entiendan mejor sin modificar el código fuente original.