mypy es el verificador de tipos estático de referencia para Python. A diferencia de los tipos en tiempo de ejecución, mypy analiza tu código sin ejecutarlo y detecta errores de tipo antes de que lleguen a producción: argumentos incorrectos, valores potencialmente None, accesos a atributos inexistentes o tipos incompatibles. Este tutorial cubre desde la instalación básica hasta el modo --strict y la integración con pre-commit.
Instalación
# pip install mypy # Stubs para bibliotecas populares: # pip install types-requests types-redis types-PyYAML
Primeros pasos con anotaciones de tipo
# calculos.py
def dividir(a: float, b: float) -> float:
if b == 0:
raise ValueError("División por cero")
return a / b
def procesar_nombres(nombres: list[str]) -> list[str]:
return [n.strip().title() for n in nombres]
# mypy detectará estos errores sin ejecutar el código:
resultado = dividir("diez", 2) # error: Argument 1 has incompatible type "str"; expected "float"
lista = procesar_nombres([1, 2, 3]) # error: List item 0 has incompatible type "int"; expected "str"
# Ejecutar mypy: # mypy calculos.py # mypy calculos.py --strict
Configurar mypy.ini
# mypy.ini [mypy] python_version = 3.13 warn_return_any = True warn_unused_configs = True ignore_missing_imports = False disallow_untyped_defs = True check_untyped_defs = True strict_optional = True no_implicit_optional = True # Relajar reglas para módulos externos sin stubs [mypy-pandas.*] ignore_missing_imports = True [mypy-celery.*] ignore_missing_imports = True
Configurar con pyproject.toml
# pyproject.toml [tool.mypy] python_version = "3.13" strict = true ignore_missing_imports = true exclude = ["tests/fixtures/", "migrations/"]
Modo strict: la configuración más exigente
--strict activa estas opciones adicionales:
--disallow-any-generics: prohíbelistsin parámetro de tipo--disallow-untyped-defs: todas las funciones deben estar anotadas--disallow-untyped-calls: no puedes llamar a funciones sin anotar--warn-return-any: advierte cuando se devuelveAny--no-implicit-reexport: los imports deben reexportarse explícitamente
# Con --strict, esto falla:
def suma(a, b): # error: Function is missing a type annotation
return a + b
def lista() -> list: # error: Missing type parameters for generic type "list"
return []
# Correcto:
def suma(a: int, b: int) -> int:
return a + b
def lista() -> list[int]:
return []
reveal_type: inspeccionar tipos inferidos
from typing import Optional
def buscar_usuario(uid: int) -> Optional[dict[str, str]]:
if uid > 0:
return {"nombre": "Ana", "email": "[email protected]"}
return None
usuario = buscar_usuario(42)
reveal_type(usuario) # mypy imprime: Revealed type is "Optional[Dict[str, str]]"
if usuario is not None:
reveal_type(usuario) # Revealed type is "Dict[str, str]"
print(usuario["nombre"])
cast: forzar el tipo cuando mypy no puede inferirlo
from typing import cast
import json
datos_json = '{"nombre": "Python", "version": 3}'
datos = json.loads(datos_json) # mypy infiere: Any
# Informamos a mypy del tipo real
datos_tipado = cast(dict[str, object], datos)
reveal_type(datos_tipado) # Dict[str, object]
type: ignore y noqa
# Para suprimir un error específico (úsalo con moderación)
resultado = dividir("texto", 2) # type: ignore[arg-type]
# Mejor: suprimir solo el código de error específico
import os
nombre: str = os.getenv("APP_NAME") # type: ignore[assignment]
# Sin type:ignore, sería necesario manejar el None:
nombre = os.getenv("APP_NAME") or "miapp" # más correcto
TypeVar y Generics
from typing import TypeVar, Generic
T = TypeVar('T')
class Pila(Generic[T]):
def __init__(self) -> None:
self._datos: list[T] = []
def push(self, valor: T) -> None:
self._datos.append(valor)
def pop(self) -> T:
if not self._datos:
raise IndexError("Pila vacía")
return self._datos.pop()
def __len__(self) -> int:
return len(self._datos)
pila_int: Pila[int] = Pila()
pila_int.push(1)
pila_int.push(2)
valor = pila_int.pop()
reveal_type(valor) # int
pila_int.push("texto") # error: Argument 1 has incompatible type "str"
Protocol: tipado estructural (duck typing)
from typing import Protocol, runtime_checkable
@runtime_checkable
class Logeable(Protocol):
def log(self, mensaje: str) -> None:
...
class LoggerConsola:
def log(self, mensaje: str) -> None:
print(f"[LOG] {mensaje}")
class LoggerFichero:
def __init__(self, ruta: str):
self.ruta = ruta
def log(self, mensaje: str) -> None:
with open(self.ruta, 'a') as f:
f.write(f"{mensaje}n")
def procesar(logger: Logeable, datos: list[str]) -> None:
for dato in datos:
logger.log(dato)
procesar(LoggerConsola(), ["a", "b"]) # OK
procesar(LoggerFichero("/tmp/log.txt"), ["c"]) # OK duck typing
Integración con pre-commit
# .pre-commit-config.yaml
repos:
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.11.0
hooks:
- id: mypy
args: [--strict, --ignore-missing-imports]
additional_dependencies:
- types-requests
- types-redis
mypy es más valioso en proyectos grandes donde los errores de tipo sin detectar se acumulan silenciosamente. Adoptar tipos de forma gradual empezando por las funciones públicas y los módulos más críticos es más efectivo que intentar anotar todo de golpe.
