Type hints en Python y verificación con mypy
1. Descripción general
Python es un lenguaje con tipado dinámico, lo que significa que no necesitas declarar el tipo de dato de antemano. Esto es flexible pero propenso a errores en tiempo de ejecución conforme el proyecto crece. Type hints (PEP 484) te permiten anotar tipos de datos sin cambiar el comportamiento en tiempo de ejecución, mientras que mypy lee esas anotaciones para detectar errores de tipo mientras escribes el código.
Después de leer este artículo, sabrás cómo escribir type hints para variables, funciones, clases, usar Optional, Union, TypedDict, genéricos, e integrar mypy en tu proceso de verificación de código.
2. Type hints básicos
2.1. Declarar tipos para variables y funciones
def calculate_total(price: float, quantity: int) -> float:
return price * quantity
customer_name: str = "Juan García"
total: float = calculate_total(price=15.5, quantity=3)
print(total)
price: float,quantity: intson los tipos de los parámetros de entrada.-> floates el tipo del valor devuelto.customer_name: stres una anotación para la variable, que ayuda al IDE a sugerir código y a mypy a verificar.
46.5
Nota: Los type hints NO fuerzan tipos en tiempo de ejecución. Si pasas
calculate_total("10", 2), Python seguirá ejecutándose normalmente porque el intérprete ignora las anotaciones — solo herramientas como mypy reportarán el error.
2.2. Instalar mypy
pip install mypy
Verificar un archivo:
mypy order_service.py
Si no hay errores, mypy imprime:
Success: no issues found in 1 source file
3. Optional y Union para valores que pueden ser None
Muchas funciones devuelven None cuando no encuentran datos. Usa Optional para expresar esto de manera explícita.
from typing import Optional
class Customer:
def __init__(self, customer_id: int, email: str) -> None:
self.customer_id = customer_id
self.email = email
def find_customer(customer_id: int) -> Optional[Customer]:
database = {1: Customer(1, "a@example.com")}
return database.get(customer_id)
customer = find_customer(2)
if customer is not None:
print(customer.email)
Optional[Customer]es equivalente aUnion[Customer, None].- Si llamas a
customer.emailsin verificarNoneprimero, mypy reportará el errorItem "None" of "Optional[Customer]" has no attribute "email".
from typing import Union
def format_id(value: Union[int, str]) -> str:
return str(value).zfill(6)
Union[int, str] permite que el parámetro sea int o str. A partir de Python 3.10, puedes escribir de manera más concisa usando int | str en lugar de Union[int, str] y Customer | None en lugar de Optional[Customer].
4. Tipos de colecciones: list, dict, tuple
from typing import Dict, List, Tuple
def summarize_orders(order_totals: List[float]) -> Tuple[float, int]:
return sum(order_totals), len(order_totals)
def build_price_table(items: Dict[str, float]) -> Dict[str, float]:
return {name: price * 1.1 for name, price in items.items()}
List[float]significa una lista de números flotantes.Tuple[float, int]significa una tupla con exactamente 2 elementos:floaty luegoint.Dict[str, float]es un diccionario con claves de tipostry valores de tipofloat.
Nota: A partir de Python 3.9, puedes usar directamente
list[float],dict[str, float]sin necesidad de importar desdetyping. Usatyping.List/typing.Dictprincipalmente para compatibilidad con Python 3.8 y versiones anteriores.
5. TypedDict para datos de diccionario con estructura
Cuando trabajas con JSON o diccionarios con esquema fijo, TypedDict ayuda a mypy a verificar cada clave.
from typing import TypedDict
class OrderPayload(TypedDict):
order_id: int
customer_email: str
total: float
def create_order(payload: OrderPayload) -> str:
return f"Pedido #{payload['order_id']} - {payload['customer_email']} - {payload['total']}"
payload: OrderPayload = {
"order_id": 101,
"customer_email": "b@example.com",
"total": 250.0,
}
print(create_order(payload))
Si te falta la clave total o escribes mal "totl", mypy reporta:
error: Missing key "total" for TypedDict "OrderPayload"
Esta es una gran ventaja sobre usar Dict[str, Any] — controlas la estructura de datos mientras mantienes un diccionario Python puro en tiempo de ejecución.
6. Genéricos con TypeVar
Cuando escribes una función o clase que se usa con múltiples tipos de datos, usas TypeVar para mantener la relación entre el tipo de entrada y salida.
from typing import TypeVar, List
T = TypeVar("T")
def first_item(items: List[T]) -> T:
return items[0]
first_name: str = first_item(["Carlos", "Ana"])
first_number: int = first_item([10, 20, 30])
TypeVar("T")crea un marcador de posición de tipo.- Cuando llamas
first_item(["Carlos", "Ana"]), mypy deduceT = str, por lo que el resultado devuelto esstr. - Cuando llamas con
List[int],T = int. De esta manera, mypy sabe exactamente qué tipo se devuelve en cada llamada, en lugar de simplementeAny.
7. Class y Protocol en lugar de interface
Python no tiene interfaces como Java, pero Protocol (PEP 544) te permite definir tipado estructural — verificar “tiene el método correcto” en lugar de necesitar herencia.
from typing import Protocol
class PaymentGateway(Protocol):
def charge(self, amount: float) -> bool:
...
class VnPayGateway:
def charge(self, amount: float) -> bool:
print(f"Cobrando {amount} a través de VNPay")
return True
def process_payment(gateway: PaymentGateway, amount: float) -> bool:
return gateway.charge(amount)
process_payment(VnPayGateway(), 199000.0)
VnPayGateway no necesita heredar de PaymentGateway — mypy la acepta si solo tiene el método charge(self, amount: float) -> bool que cumple con Protocol. Esta es una forma muy “Pythonic” de trabajar, manteniendo el duck typing pero teniendo verificación estática.
8. Configurar mypy para el proyecto
Añade la dependencia a requirements-dev.txt:
mypy==1.11.2
Crea el archivo de configuración mypy.ini en el directorio raíz:
[mypy]
python_version = 3.12
disallow_untyped_defs = True
warn_return_any = True
warn_unused_ignores = True
ignore_missing_imports = True
disallow_untyped_defs = Truerequiere que todas las funciones tengan type hints, evitando omisiones.warn_return_any = Trueadvierte cuando una función devuelveAnyde manera silenciosa.ignore_missing_imports = Truees útil al usar librerías de terceros que aún no tienen type stubs.
Ejecuta la verificación en todo el proyecto:
mypy src/
9. Escribir tests para el comportamiento con type hints
Los type hints no reemplazan los tests, pero ayudan a que los tests sean más claros sobre los tipos de entrada/salida. Usa pytest:
<!-- equivalente a requirements-dev.txt, solo para ilustrar la estructura de dependencias -->
pytest==8.3.2
from order_service import calculate_total
def test_calculate_total_returns_correct_sum() -> None:
result = calculate_total(price=20.0, quantity=5)
assert result == 100.0
def test_calculate_total_with_zero_quantity() -> None:
result = calculate_total(price=50.0, quantity=0)
assert result == 0.0
Ejecuta los tests:
pytest -v
test_order_service.py::test_calculate_total_returns_correct_sum PASSED
test_order_service.py::test_calculate_total_with_zero_quantity PASSED
Puedes combinar ambos pasos en CI: ejecuta mypy primero para detectar errores de tipo, luego pytest para verificar la lógica de negocio. Ambas herramientas se complementan — mypy detecta errores de “tipo incorrecto”, pytest detecta errores de “lógica incorrecta”.
10. Errores comunes y cuándo no necesitas mypy
- Olvidar actualizar type hints cuando cambias la lógica: por ejemplo, una función cambia de devolver
Optional[Customer]a siempre devolverCustomer, pero olvidas actualizar la anotación — mypy no reportará un error si el código sigue coincidiendo con el tipo más amplio anterior, resultando en información de anotación inexacta. - Usar
Anyen todas partes: declarardef process(data: Any) -> Anyhace que mypy pierda su capacidad de verificación, deshabilitando efectivamente el tipado estático en esa sección. - Anotaciones incorrectas con librerías de terceros sin stubs: muchos paquetes antiguos aún no publican type stubs, mypy reportará
error: Cannot find implementation or library stub. Puedes manejarlo conignore_missing_importso instalandotypes-<package>si está disponible. - Cuándo NO deberías usar mypy estrictamente: con scripts pequeños, scripts de un solo uso (throwaway scripts), o prototipos rápidos para probar ideas — requerir type hints completos puede ralentizar tu velocidad de escritura sin proporcionar valor correspondiente.
Nota: mypy solo hace análisis estático (static analysis) en el código fuente, no ejecuta el programa. No puede detectar errores que dependan de datos en tiempo de ejecución, por ejemplo, valores leídos de archivos de configuración o APIs externas que tengan el tipo correcto según se declaró.
11. Conclusión
Los type hints hacen que el código Python sea más legible, más fácil de mantener y reducen los errores por confusión de tipos, mientras que mypy convierte esas anotaciones en herramientas de verificación reales en lugar de solo documentación. Deberías comenzar aplicándolo gradualmente en funciones públicas, módulos críticos, luego expandirlo progresivamente a todo el proyecto usando la configuración disallow_untyped_defs. Integrar mypy en CI junto con tests usando pytest es la forma más práctica de mantener la flexibilidad de Python mientras logras seguridad cercana a la de un lenguaje con tipado estático.