Python es un lenguaje dinámico: no exige declarar tipos para ejecutar un programa. Sin embargo, las anotaciones de tipo ayudan a comunicar intención, documentar contratos y detectar errores antes de ejecutar.
En este tema usaremos type hints de forma gradual y revisaremos el código con mypy. El objetivo no es convertir Python en otro lenguaje, sino mejorar claridad y reducir errores comunes.
Los type hints son anotaciones que indican qué tipos se esperan en parámetros, variables o valores de retorno.
def calcular_importe(precio: float, cantidad: int) -> float:
return precio * cantidad
Python no impide ejecutar la función con otros tipos. Las anotaciones sirven para herramientas, editores y lectores del código.
Las anotaciones de tipo ayudan porque hacen explícito el contrato de una función. Al leer la firma, sabemos qué datos espera y qué devuelve.
Tipado gradual significa que no hace falta anotar todo el proyecto de una vez. Podemos empezar por funciones importantes, modelos de datos y límites entre módulos.
Una estrategia razonable:
Algunos tipos frecuentes:
def saludar(nombre: str) -> str:
return f"Hola, {nombre}"
def es_mayor_de_edad(edad: int) -> bool:
return edad >= 18
def calcular_total(precio: float, cantidad: int) -> float:
return precio * cantidad
La flecha -> indica el tipo de retorno.
En Python moderno podemos escribir tipos genéricos con list y dict.
def sumar_valores(valores: list[float]) -> float:
return sum(valores)
def obtener_email(usuario: dict[str, str]) -> str:
return usuario["email"]
Los diccionarios con muchas claves suelen ser difíciles de tipar con precisión. En esos casos, una dataclass puede ser más clara.
Las dataclasses combinan muy bien con type hints.
from dataclasses import dataclass
@dataclass(frozen=True)
class Producto:
precio: float
cantidad: int
def calcular_importe(producto: Producto) -> float:
return producto.precio * producto.cantidad
La estructura del dato y la firma de la función quedan claras.
Cuando un valor puede ser None, debemos indicarlo.
def buscar_producto(codigo: str) -> Producto | None:
if codigo == "A1":
return Producto(precio=1000, cantidad=1)
return None
Quien llama debe manejar el caso None:
producto = buscar_producto("A1")
if producto is not None:
print(producto.precio)
Activa el entorno virtual del proyecto y ejecuta:
python -m pip install mypy
Verifica la instalación:
python -m mypy --version
Ejecuta mypy sobre la carpeta src:
python -m mypy src
Si el proyecto todavía tiene pocas anotaciones, mypy puede informar pocos problemas. A medida que agregamos tipos, la herramienta puede revisar más contratos.
Agrega una configuración inicial:
[tool.mypy]
python_version = "3.10"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = false
check_untyped_defs = true
Esta configuración permite empezar gradualmente. Más adelante se puede volver más estricta.
Crea src/tipos_demo.py:
def calcular_importe(precio: float, cantidad: int) -> float:
return precio * cantidad
total = calcular_importe("1000", 2)
Ejecuta:
python -m mypy src/tipos_demo.py
mypy debería señalar que el primer argumento espera float, pero recibe str.
Si ya creaste dataclasses en modelos.py, puedes anotar funciones de venta:
from modelos import Producto
def calcular_subtotal(productos: list[Producto]) -> float:
return sum(
producto.precio * producto.cantidad
for producto in productos
)
def obtener_descuento(tipo_cliente: str) -> float:
return {"vip": 0.15, "regular": 0.05}.get(tipo_cliente, 0)
Las firmas comunican claramente qué datos necesita cada función.
Any desactiva gran parte del beneficio del tipado. A veces es necesario, pero conviene no usarlo como salida rápida.
from typing import Any
def procesar(datos: Any) -> Any:
return datos["valor"] * 2
Si conocemos la estructura, es mejor expresarla:
def duplicar_valor(datos: dict[str, float]) -> float:
return datos["valor"] * 2
Un alias de tipo puede mejorar la lectura cuando una estructura se repite.
ProductoDict = dict[str, float]
def calcular_importe(producto: ProductoDict) -> float:
return producto["precio"] * producto["cantidad"]
Si la estructura crece, una dataclass suele ser mejor que un alias de diccionario.
En casos más avanzados, un protocolo permite indicar que esperamos un objeto con cierto método, sin depender de una clase concreta.
from typing import Protocol
class CalculaTotal(Protocol):
def calcular_total(self) -> float:
...
def generar_ticket(venta: CalculaTotal) -> str:
return f"Total: {venta.calcular_total()}"
No hace falta empezar por protocolos. Son útiles cuando el proyecto ya tiene varias clases con el mismo contrato.
mypy no reemplaza las pruebas. Los tipos ayudan a detectar inconsistencias estructurales; las pruebas verifican comportamiento.
python -m mypy src
python -m pytest
Ambas herramientas se complementan. Un código puede pasar mypy y tener una regla de negocio incorrecta.
Agrega tipos a este código:
def aplicar_descuento(total, descuento):
return total * (1 - descuento)
def obtener_descuento(cliente):
if cliente == "vip":
return 0.15
return 0
Una posible solución:
def aplicar_descuento(total: float, descuento: float) -> float:
return total * (1 - descuento)
def obtener_descuento(cliente: str) -> float:
if cliente == "vip":
return 0.15
return 0
En ventas_demo, realiza estas tareas:
pyproject.toml.ventas.py.python -m mypy src
python -m ruff check src tests
python -m black src tests
python -m pytest
Antes de continuar, verifica que puedes hacer lo siguiente:
str, int, float y bool.| None.En este tema vimos que los type hints ayudan a expresar contratos y mejorar la comprensión del código. mypy permite revisar esas anotaciones y detectar inconsistencias antes de ejecutar el programa.
En el próximo tema trabajaremos con interfaces claras: funciones, módulos y contratos fáciles de entender.