Type Checking Estricto en Python con mypy
Cómo configurar mypy strict mode para proyectos Python, manejar errores de tipo comunes, usar Protocol y TypeGuard, e integrar con CI/CD.
Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.
Overview
mypy es un static type checker para Python. En strict mode, enforza type annotations en todas las funciones, atrapa None donde se esperaba int, previene uso inseguro de Any, y flagea return statements faltantes. El type checking estricto atrapa bugs en tiempo de desarrollo que de otra forma surgirían en producción — AttributeError en None, argumentos de tipo equivocado, valores Optional no manejados.
When to Use
- Codebases de Python de producción donde los bugs en runtime son costosos
- Librerías y APIs consumidas por otros equipos
- Codebases con data flows complejos donde los types ayudan a la navegación
- Cuando onboarding nuevos developers — los types sirven como documentación
- Pipelines CI/CD para enforzar type safety antes del merge
When NOT to Use
- Scripts rápidos o prototipos — el overhead de annotation no vale la pena
- Codebases legacy sin annotations existentes — empezá gradual, no strict
- Jupyter notebooks — mypy no se integra bien con workflows de notebook
- Cuando el equipo no tiene experiencia con TypeScript/mypy — empezá con basic mode primero
Solution
Instalar mypy
pip install mypy
# Con extensiones útiles
pip install mypy types-requests types-pyyaml
# Usando poetry
poetry add --group dev mypy
Configuración básica
# mypy.ini
[mypy]
python_version = 3.12
strict = True
warn_return_any = True
warn_unused_ignores = True
warn_redundant_casts = True
warn_unreachable = True
disallow_untyped_defs = True
disallow_untyped_decorators = True
disallow_any_generics = True
no_implicit_optional = True
check_untyped_defs = True
show_error_codes = True
show_column_numbers = True
pretty = True
Configuración en pyproject.toml
# pyproject.toml
[tool.mypy]
python_version = "3.12"
strict = true
warn_return_any = true
warn_unused_ignores = true
warn_redundant_casts = true
warn_unreachable = true
show_error_codes = true
show_column_numbers = true
pretty = true
# Overrides por módulo
[[tool.mypy.overrides]]
module = "legacy.*"
strict = false
ignore_missing_imports = true
[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false
Anotar funciones
from typing import Optional
# BAD — sin annotations, mypy strict va a error
def process_data(data):
return data["key"]
# GOOD — annotations completas
def process_data(data: dict[str, str]) -> str:
return data["key"]
# GOOD — manejar Optional explícitamente
def find_user(user_id: int) -> Optional[dict[str, str]]:
user = db.get(user_id)
if user is None:
return None
return user
# GOOD — caller maneja Optional
user = find_user(42)
if user is not None:
print(user["name"])
Errores comunes de mypy y fixes
error: Missing return statement
# BAD — mypy: Missing return statement
def get_status(code: int) -> str:
if code == 200:
return "OK"
elif code == 404:
return "Not Found"
# ¿Qué pasa con otros códigos?
# GOOD — return exhaustivo
def get_status(code: int) -> str:
if code == 200:
return "OK"
elif code == 404:
return "Not Found"
return "Unknown"
# GOOD — usar assert_never para checking exhaustivo
from typing_extensions import assert_never
def get_status(code: int) -> str:
if code == 200:
return "OK"
elif code == 404:
return "Not Found"
else:
assert_never(f"Unexpected code: {code}")
error: Argument has incompatible type
# BAD — mypy: Argument 1 has incompatible type "None"
def greet(name: str) -> str:
return f"Hello, {name}"
user_input: str | None = get_input()
greet(user_input) # Error: None no compatible con str
# GOOD — chequear None primero
if user_input is not None:
greet(user_input)
error: Item “None” of “Optional[X]” has no attribute
# BAD — mypy: Item "None" of "Optional[User]" has no attribute "name"
user: User | None = get_user(42)
print(user.name) # Error
# GOOD — narrow the type
if user is not None:
print(user.name)
# GOOD — usar assert (mypy narrow después de assert)
assert user is not None
print(user.name)
error: Returning Any from function
# BAD — mypy: Returning Any from function declared to return str
import json
def get_config() -> str:
return json.loads('{"key": "value"}') # json.loads retorna Any
# GOOD — cast o validar
from typing import cast
import json
def get_config() -> dict[str, str]:
data = json.loads('{"key": "value"}')
return cast(dict[str, str], data)
# MEJOR — usar TypedDict
from typing import TypedDict
class Config(TypedDict):
key: str
def get_config() -> Config:
data = json.loads('{"key": "value"}')
return Config(**data)
Usar Protocol para structural typing
from typing import Protocol
class Readable(Protocol):
def read(self, size: int = -1) -> bytes: ...
def process_stream(stream: Readable) -> str:
data = stream.read(1024)
return data.decode("utf-8")
# Cualquier objeto con un método read() funciona — sin herencia necesaria
class MyReader:
def read(self, size: int = -1) -> bytes:
return b"hello"
process_stream(MyReader()) # OK — estructuralmente compatible
process_stream(open("file.txt", "rb")) # OK — file objects tienen read()
Usar TypeGuard para custom type narrowing
from typing import TypeGuard, Any
def is_str_list(value: list[Any]) -> TypeGuard[list[str]]:
return all(isinstance(item, str) for item in value)
def process_items(items: list[Any]) -> None:
if is_str_list(items):
# mypy sabe que items es list[str] aquí
for item in items:
print(item.upper()) # OK — método de str
Usar @overload para múltiples signatures
from typing import overload, Literal
@overload
def get_value(key: str, default: str) -> str: ...
@overload
def get_value(key: str, default: int) -> int: ...
@overload
def get_value(key: str, default: None) -> str | None: ...
def get_value(key: str, default: str | int | None) -> str | int | None:
val = db.get(key)
if val is None:
return default
return val
# mypy sabe el return type basado en el argumento
name: str = get_value("name", "anonymous") # OK
count: int = get_value("count", 0) # OK
maybe: str | None = get_value("key", None) # OK
Generic classes
from typing import Generic, TypeVar
T = TypeVar("T")
class Stack(Generic[T]):
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T | None:
if not self._items:
return None
return self._items.pop()
# Type inference desde el uso
stack: Stack[int] = Stack()
stack.push(42)
value = stack.pop() # mypy sabe: int | None
Inline type: ignore
# Suprimir un error code específico
result = some_untyped_function() # type: ignore[no-untyped-call]
# Suprimir todos los errors en una línea (usar con moderación)
data = legacy_parse(input_data) # type: ignore
Integración con CI/CD
# .github/workflows/mypy.yml
name: mypy Type Check
on: [push, pull_request]
jobs:
mypy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: |
pip install mypy
pip install -r requirements.txt
- name: Run mypy
run: mypy src/ --strict --show-error-codes --junit-xml mypy-report.xml
- name: Upload report
if: always()
uses: actions/upload-artifact@v4
with:
name: mypy-report
path: mypy-report.xml
Pre-commit hook
# .pre-commit-config.yaml
repos:
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.11.0
hooks:
- id: mypy
args: [--strict, --show-error-codes]
additional_dependencies: [types-requests, types-pyyaml]
exclude: ^(tests/|migrations/)
Variants
Gradual typing para código legacy
# pyproject.toml — empezar lenient, tightenar over time
[tool.mypy]
python_version = "3.12"
disallow_untyped_defs = false # Empezar aquí
check_untyped_defs = true
warn_redundant_casts = true
warn_unused_ignores = true
# Tightenar módulos específicos que ya están anotados
[[tool.mypy.overrides]]
module = "src.models.*"
strict = true
[[tool.mypy.overrides]]
module = "src.api.*"
strict = true
mypy con Pydantic
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str
email: str | None = None
# mypy entiende Pydantic models
user = User(id=1, name="Alice")
user_id: int = user.id # OK — mypy sabe que id es int
user_name: str = user.name # OK
mypy con SQLAlchemy
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy import String
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(100))
email: Mapped[str | None] = mapped_column(String(255), nullable=True)
# mypy conoce los types desde Mapped
user = session.get(User, 1)
if user:
print(user.name) # str
print(user.email) # str | None
Best Practices
-
For a deeper guide, see Find Security Issues in Python Code with Bandit.
-
Empezá con
strict = Truepara proyectos nuevos — atrapar issues early es más fácil que retrofitting -
Usá
show_error_codes = True— los error codes hacen# type: ignore[code]preciso -
Usá
pretty = True— output legible con context lines -
Overrides por módulo para código legacy — no bloquees todo el codebase por un módulo
-
Instalá
types-*stub packages — provee types para librerías de terceros -
Usá
assert_neverpara checking exhaustivo de enum/union — atrapa cases faltantes -
Corré en pre-commit y CI/CD — enforzá types antes del merge
-
Revisá los comentarios
# type: ignoreperiódicamente — algunos se vuelven innecesarios a medida que los types mejoran
Common Mistakes
- Empezar con strict en un codebase legacy: cientos de errors van a abrumar al equipo. Empezá lenient y tightená por módulo.
- Usar
# type: ignoresin error codes:# type: ignoresuprime todo. Usá# type: ignore[no-untyped-call]para suprimir errors específicos. - No instalar type stubs: librerías de terceros como
requestsnecesitantypes-requestspara que mypy las entienda. - Ignorar
warn_unused_ignores: si un# type: ignorese vuelve innecesario, este flag lo atrapa. - No usar
Optionalexplícitamente:def f(x: int = None)no es válido en strict mode. Usádef f(x: int | None = None).
FAQ
¿Qué es mypy strict mode?
Una configuración que habilita todos los flags de type-checking: disallow_untyped_defs, warn_return_any, disallow_any_generics, no_implicit_optional, y más. Atrapa la mayoría de los bugs relacionados con types pero requiere annotations completas.
¿Cómo manejo librerías de terceros sin type stubs?
Instalá types-* packages (e.g., types-requests). Si no existen stubs, usá [[tool.mypy.overrides]] con ignore_missing_imports = true para ese módulo.
¿Cuál es la diferencia entre mypy y pyright?
mypy es el original Python type checker, más lento pero estable. pyright (usado por Pylance) es más rápido y agresivo. Ambos siguen PEP 484. Usá mypy para CI/CD, pyright para IDE feedback.
¿Debería usar Optional[X] o X | None?
Ambos son equivalentes. X | None es la sintaxis moderna (Python 3.10+). Optional[X] es la sintaxis older. Usá X | None para código nuevo.
¿Cómo tipeo un dictionary con keys específicas?
Usá TypedDict:
class UserDict(TypedDict):
id: int
name: str
email: str | None
Esto te da type checking en dict keys y values.
Recursos Relacionados
Find Security Issues in Python Code with Bandit
How to use Bandit to scan Python code for common security vulnerabilities, configure ignore lists, integrate with CI/CD, and interpret results.
RecipeScan Python Packages for Known CVEs with pip-audit
How to use pip-audit to scan Python dependencies for known vulnerabilities, configure ignore lists, integrate with CI/CD, and remediate findings.
RecipeShare Workflow Logic with GitHub Actions Reusable Workflows
How to create and consume reusable workflows in GitHub Actions, covering inputs, secrets, conditional jobs, matrix strategy, and organization-wide sharing.
RecipeDetect Bugs in Java with SpotBugs Static Analysis
How to configure SpotBugs for Maven and Gradle, interpret bug patterns, suppress false positives, and integrate with CI/CD pipelines.
RecipeStrict TypeScript ESLint Configuration for Production
How to configure typescript-eslint with strict rules for production TypeScript projects, handle type-aware linting, and integrate with CI/CD.