mypy usa anotaciones para detectar llamadas incompatibles antes de ejecutar el programa. Python sigue siendo dinámico, por lo que el tipado complementa las pruebas y no las reemplaza.

Configurar pyproject.toml

[tool.mypy]
python_version = "3.13"
warn_unused_ignores = true
disallow_untyped_defs = true
no_implicit_optional = true
from collections.abc import Iterable


def total(valores: Iterable[int]) -> int:
    return sum(valores)

Ejecuta mypy src. En código antiguo, empieza por módulos de dominio y bibliotecas compartidas. Evita nuevas funciones sin anotación antes de migrar todo. La guía de type hints explica unions, genéricos y protocolos.

Busca paquetes de stubs mantenidos para dependencias sin tipos. Usa # type: ignore[codigo] solo en la línea necesaria, con una razón, y activa warn_unused_ignores. No propagues Any solo para silenciar diagnósticos.

Ejecuta el mismo comando en CI. La guía de tox en Python muestra cómo coordinar tipado y matrices.

La documentación oficial de mypy, consultada el 22 de julio de 2026, explica adopción gradual y modo estricto. El objetivo es hacer contratos importantes explícitos, no satisfacer la herramienta a cualquier precio.

Entender qué comprueba mypy

mypy analiza valores anotados sin ejecutar el programa. Puede detectar que una función esperaba str y recibió un int, seguir el tipo después de isinstance y comprobar que todas las rutas devuelven el resultado declarado.

La garantía tiene límites. JSON, variables de entorno, bases de datos y respuestas de red todavía requieren validación durante la ejecución. Una anotación no convierte ni inspecciona entradas externas. Las pruebas siguen cubriendo comportamiento, integraciones, tiempo y efectos secundarios. El análisis estático mantiene contratos coherentes; la suite confirma que la implementación los cumple.

Any crea otra frontera. Casi todas sus operaciones se aceptan, por lo que un valor impreciso puede transportar un error por varias funciones. Úsalo cuando una interfaz carezca realmente de tipos, valida o convierte el dato cerca de la entrada y devuelve un tipo preciso al resto de la aplicación.

Crear una base práctica

Guarda la configuración en el repositorio para que editor, terminal y CI apliquen las mismas reglas:

[tool.mypy]
files = ["src", "tests"]
python_version = "3.13"
show_error_codes = true
warn_unused_ignores = true
warn_redundant_casts = true
check_untyped_defs = true

check_untyped_defs analiza cuerpos sin anotaciones completas, pero no convierte sus interfaces en tipadas. disallow_untyped_defs es el siguiente paso estricto. En sistemas existentes, actívalo primero para módulos nuevos o ya migrados.

Ejecuta el comando desde la raíz. El descubrimiento de configuración y las rutas de importación pueden cambiar desde otro directorio. Fija mypy y los stubs en las dependencias de desarrollo, porque sus actualizaciones pueden descubrir errores legítimos nuevos.

Migrar por fronteras

Una unidad útil es un paquete coherente, no una cantidad arbitraria de avisos. Empieza por modelos de dominio y utilidades compartidas, cuyas anotaciones mejoran la inferencia de sus consumidores. Tipifica funciones públicas y luego los helpers internos.

[[tool.mypy.overrides]]
module = "informes_antiguos.*"
disallow_untyped_defs = false

[[tool.mypy.overrides]]
module = "facturacion.*"
disallow_untyped_defs = true
disallow_any_generics = true

La excepción registra la deuda pendiente y protege el código migrado. Evita ignore_errors global, que oculta regresiones junto al inventario inicial. Limita cada override y elimínalo cuando termine la migración.

No anotes todo como object o Any para alcanzar cero errores. object permite pocas operaciones; Any permite casi todas y desplaza el riesgo. Un protocolo puede describir exactamente las operaciones que necesita el consumidor.

Modelar valores opcionales

str | None indica que la ausencia es un caso real, algo distinto de ofrecer un valor predeterminado. Haz la comprobación explícita:

def nombre_visible(nombre: str | None) -> str:
    if nombre is None:
        return "Anónimo"
    return nombre.strip()

Evita assert valor is not None cuando la ausencia pueda suceder. Devuelve, lanza una excepción significativa o valida el objeto al construirlo. Una aserción usada para convencer al analizador puede ocultar una regla incompleta.

Un predicado complejo puede declarar TypeGuard, pero solo si verifica todas las condiciones prometidas. mypy confía en esa declaración, así que un guard incorrecto resulta tan peligroso como un cast sin comprobación.

Conservar relaciones con genéricos

Los genéricos mantienen la relación entre entrada y salida. Una función que devuelve el primer elemento debe conservar su tipo:

from collections.abc import Sequence
from typing import TypeVar

T = TypeVar("T")


def primero(elementos: Sequence[T]) -> T:
    if not elementos:
        raise ValueError("elementos no puede estar vacío")
    return elementos[0]

Los protocolos definen contratos estructurales. Una función acepta cualquier objeto con los métodos necesarios sin exigir herencia de una clase del framework. Mantenlos pequeños y centrados en el consumidor. Un protocolo enorme suele indicar demasiadas responsabilidades.

Usa cast() solo si la lógica de ejecución ya estableció información que mypy no puede inferir. El cast no convierte ni comprueba nada. Si el valor es incierto, valida con isinstance, un parser o un esquema.

Tratar dependencias sin tipos

Algunos paquetes incluyen información mediante py.typed; otros usan distribuciones de stubs. Sigue las instrucciones oficiales y actualiza pares compatibles. Un stub antiguo puede contradecir la biblioteca instalada.

Si no existen tipos, crea un adaptador local con interfaz precisa. Así concentras la incertidumbre. Una excepción por módulo es más segura que la opción global:

[[tool.mypy.overrides]]
module = "proveedor_sin_tipos.*"
ignore_missing_imports = true

Los archivos generados pueden tener una exclusión explícita si son reproducibles y nunca se editan. No excluyas todo el código fuente porque un módulo produzca ruido.

Diagnosticar antes de silenciar

Lee el código del error y busca dónde el tipo se volvió demasiado amplio. reveal_type(valor) ayuda durante la investigación y debe retirarse después. Las colecciones vacías suelen necesitar intención explícita:

ids_usuarios: list[int] = []

Cuando una excepción sea inevitable, limita el diagnóstico: # type: ignore[import-untyped]. Explica la razón si no es evidente. warn_unused_ignores detectará supresiones que una actualización volvió innecesarias.

No resuelvas cada incompatibilidad con cast. Tal vez la anotación reveló un retorno None no documentado, una colección que recibe el valor equivocado o una función que promete más de lo que admite.

Hacer CI estricta y predecible

CI debe invocar el mismo comando local, con idéntica configuración y dependencias. Un entorno de tox puede instalar mypy y stubs y ejecutar mypy src tests. Permite que el job falle ante diagnósticos y no ocultes el estado.

Durante la adopción gradual, fija el alcance actual y evita errores nuevos. Ampliar un paquete a la vez ofrece un criterio visible. Comparar solo la cantidad total es inseguro: un error grave podría reemplazar varios avisos antiguos mientras el número baja.

El editor ofrece respuesta rápida, pero CI es la autoridad compartida. Documenta el comando y mantenlo ágil. Ejecuta pruebas a su lado, porque tipos coherentes no prueban que el sistema funcione correctamente.

Mantener una política útil

Exige anotaciones en fronteras estables: funciones públicas, modelos compartidos, callbacks y transformaciones. Las variables locales suelen beneficiarse de la inferencia, salvo colecciones vacías o interfaces deliberadamente amplias.

Revisa los tipos como decisiones de diseño. Una union complicada puede revelar responsabilidades mezcladas; comprobaciones opcionales repetidas pueden señalar un estado inválido; Any persistente puede indicar una entrada sin validar. El resultado importante no es solo una consola limpia, sino contratos que personas y herramientas puedan entender.