mypy en Python: verificación estática de tipos, configuración, plugins y modo strict

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íbe list sin 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 devuelve Any
  • --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.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP