Type Hints (o anotaciones de tipo) son una de las características más importantes introducidas en Python 3.5 mediante la PEP 484. Te permiten indicar los tipos esperados de variables, parámetros y retornos de funciones, haciendo tu código más legible, seguro y fácil de mantener.
En esta guía completa aprenderás todo sobre Type Hints en Python: desde conceptos básicos hasta temas avanzados como Genéricos, Protocol y TypeVar, con ejemplos prácticos que podrás aplicar de inmediato en tus proyectos.
¿Qué son Type Hints?
Type Hints son anotaciones de tipo que agregas al código Python para indicar qué tipo de dato debe tener una variable, parámetro o retorno de función. Son opcionales — Python sigue siendo un lenguaje de tipado dinámico — pero herramientas como mypy, Pyright y Pylance usan estas anotaciones para realizar verificación estática de tipos.
def saludo(nombre: str) -> str:
return f"¡Hola, {nombre}!"
print(saludo("María")) # Funciona
print(saludo(42)) # mypy marcaría error
En el ejemplo anterior, nombre: str indica que el parámetro debe ser un string, y -> str indica que la función retorna un string. El código se ejecuta normalmente, pero los verificadores de tipo señalarían la segunda llamada como incorrecta.
Fuente oficial: Documentación Python - Módulo typing
¿Por qué usar Type Hints?
Adoptar Type Hints trae múltiples beneficios:
- Código más legible: otros desarrolladores entienden de inmediato qué tipos espera y retorna cada función
- Detección temprana de errores: herramientas como mypy encuentran errores de tipo antes de ejecutar el código
- Autocompletado inteligente: IDEs como VS Code, PyCharm y Vim ofrecen sugerencias más precisas basadas en tipos
- Documentación viva: los tipos funcionan como documentación auto-actualizable que nunca queda obsoleta
- Refactorización segura: al cambiar la firma de una función, el verificador señala todos los lugares que necesitan ajuste
Fuente: Real Python - Type Checking in Python
Type Hints Básicos
Tipos Simples
Los tipos más básicos son int, float, str y bool:
def calcular_area(radio: float) -> float:
return 3.14159 * radio ** 2
def es_mayor_edad(edad: int) -> bool:
return edad >= 18
def obtener_nombre() -> str:
return "Python"
def procesar(logico: bool) -> None:
if logico:
print("Verdadero")
Nota que None se usa como retorno cuando la función no devuelve nada.
Tipos de Colección
Para listas, diccionarios, tuplas y conjuntos, usa el módulo typing o, a partir de Python 3.9, los tipos estándar directamente:
from typing import List, Dict, Tuple, Set
Python 3.8 y anteriores
nombres: List[str] = ["Ana", "Juan", "María"]
precios: Dict[str, float] = {"banana": 2.50, "manzana": 3.00}
coordenada: Tuple[float, float] = (-23.55, -46.63)
identificadores: Set[int] = {1, 2, 3}
Python 3.9+
nombres: list[str] = ["Ana", "Juan", "María"]
precios: dict[str, float] = {"banana": 2.50, "manzana": 3.00}
A partir de Python 3.9 puedes usar list[str] en lugar de List[str], simplificando las importaciones. Este cambio fue propuesto en la PEP 585.
Optional y Union
Frecuentemente una variable puede ser de más de un tipo. Para eso existen Optional y Union:
from typing import Optional, Union
Optional significa que puede ser X o None
def buscar_usuario(id: int) -> Optional[str]:
usuarios = {1: "Ana", 2: "Juan"}
return usuarios.get(id) # Puede retornar str o None
Union significa que puede ser cualquiera de los tipos listados
def formatear_valor(valor: Union[int, float, str]) -> str:
if isinstance(valor, str):
return valor
return f"$ {valor:.2f}"
Python 3.10+ acepta sintaxis con pipe
def procesar_id(item_id: int | str) -> bool:
return bool(item_id)
En Python 3.10, la PEP 604 introdujo la sintaxis con pipe (int | str), haciendo el código más limpio.
Type Aliases
Para tipos complejos que se repiten, crea aliases:
from typing import List, Tuple
Type alias para coordenadas
Coordenada = Tuple[float, float]
Ruta = List[Coordenada]
def calcular_distancia(p1: Coordenada, p2: Coordenada) -> float:
from math import sqrt
return sqrt((p2[0] - p1[0])2 + (p2[1] - p1[1])2)
def planificar_ruta(puntos: Ruta) -> float:
distancia_total = 0.0
for i in range(len(puntos) - 1):
distancia_total += calcular_distancia(puntos[i], puntos[i + 1])
return distancia_total
Los aliases hacen el código más semántico y facilitan cambios futuros.
Tipado en Funciones
Parámetros con Valores por Defecto
def crear_perfil(nombre: str, edad: int = 0, activo: bool = True) -> dict:
return {"nombre": nombre, "edad": edad, "activo": activo}
*args y **kwargs
from typing import Any
def sumar_todo(*args: int) -> int:
return sum(args)
def log(mensaje: str, **kwargs: Any) -> None:
print(f"{mensaje}: {kwargs}")
Callable
Para tipar funciones recibidas como parámetro:
from typing import Callable
def ejecutar_operacion(
a: int, b: int, operacion: Callable[[int, int], int]
) -> int:
return operacion(a, b)
resultado = ejecutar_operacion(10, 5, lambda x, y: x + y)
print(resultado) # 15
Clases y Type Hints
Métodos y Self
class Persona:
def __init__(self, nombre: str, edad: int) -> None:
self.nombre = nombre
self.edad = edad
def cumpleanos(self) -> None:
self.edad += 1
def presentarse(self) -> str:
return f"{self.nombre} tiene {self.edad} años"
Atributos de Clase
class Configuracion:
version: str = "1.0"
timeout: int
debug: bool
def __init__(self, timeout: int, debug: bool = False) -> None:
self.timeout = timeout
self.debug = debug
Tipos Avanzados
Literal
Restringe un valor a constantes específicas:
from typing import Literal
def set_modo(modo: Literal["rápido", "lento", "eco"]) -> None:
print(f"Modo {modo} activado")
set_modo("rápido") # OK
set_modo("turbo") # Error de tipo
TypedDict
Define la estructura de diccionarios:
from typing import TypedDict
class Usuario(TypedDict):
nombre: str
email: str
edad: int
def crear_usuario(datos: Usuario) -> Usuario:
return datos
user: Usuario = {"nombre": "Ana", "email": "[email protected]", "edad": 30}
Final
Indica que un valor no debe ser sobrescrito:
from typing import Final
MAX_INTENTOS: Final[int] = 3
PI: Final[float] = 3.14159
Los verificadores marcarían error al intentar modificar
MAX_INTENTOS = 5 # Error!
Genéricos y TypeVar
Los genéricos permiten crear funciones y clases que funcionan con múltiples tipos de forma segura:
from typing import TypeVar, Generic, List
T = TypeVar("T") # TypeVar genérico
def primer_elemento(lista: List[T]) -> T:
return lista[0]
print(primer_elemento([1, 2, 3])) # int
print(primer_elemento(["a", "b"])) # str
TypeVar con Restricción
from typing import TypeVar
Numero = TypeVar("Numero", int, float)
def sumar_valores(a: Numero, b: Numero) -> Numero:
return a + b
sumar_valores(1, 2) # OK
sumar_valores(1.5, 2.5) # OK
sumar_valores("a", "b") # Error!
Generic en Clases
from typing import Generic, TypeVar, List
T = TypeVar("T")
class Pila(Generic[T]):
def init(self) -> None:
self._items: List[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
pila_int = Pila[int]()
pila_int.push(10)
pila_int.push(20)
print(pila_int.pop()) # 20
pila_str = Pila[str]()
pila_str.push("Python")
print(pila_str.pop()) # Python
Protocol (Subtipado Estructural)
Protocol, introducido en la PEP 544, permite duck typing estático:
from typing import Protocol
class Hablador(Protocol):
def hablar(self) -> str:
...
class Persona:
def hablar(self) -> str:
return "¡Hola!"
class Robot:
def hablar(self) -> str:
return "Beep boop"
def saludar(entidad: Hablador) -> None:
print(entidad.hablar())
saludar(Persona()) # ¡Hola!
saludar(Robot()) # Beep boop
Con Protocol, cualquier clase que implemente el método hablar es aceptada sin necesidad de heredar de una clase base.
Anotaciones en Variables
También puedes anotar variables directamente:
from typing import List, Optional
nombre: str = "Python"
version: float = 3.12
tags: List[str] = ["dinámico", "versátil", "moderno"]
direccion: Optional[str] = None
Herramientas de Verificación de Tipos
Las principales herramientas para verificación estática de tipos en Python son:
- mypy: la herramienta estándar, creada por Jukka Lehtosalo bajo la guía de Guido van Rossum. Soporta la mayoría de funcionalidades del módulo typing.
- Pyright: herramienta de Microsoft usada por Pylance (extensión de VS Code). Extremadamente rápida con excelente soporte de tipos.
- pyre: herramienta de Facebook (Meta) para type checking en Python.
Sitio oficial: Sitio oficial de mypy
Type Hints con Dataclasses
Las dataclasses, introducidas en Python 3.7, combinan perfectamente con Type Hints. Consulta también nuestra guía completa sobre Data Classes en Python.
from dataclasses import dataclass
from typing import List, Optional
@dataclass
class Producto:
nombre: str
precio: float
cantidad: int = 0
categorias: Optional[List[str]] = None
def valor_total(self) -> float:
return self.precio * self.cantidad
producto = Producto("Notebook", 4500.00, 2)
print(f"Total: $ {producto.valor_total():.2f}")
Documentación oficial: Python Data Classes
Type Hints con FastAPI y Pydantic
FastAPI usa Type Hints intensamente para validación automática de datos y generación de documentación. Para aprender más sobre APIs, visita nuestra guía de FastAPI: Creando APIs RESTful.
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List, Optional
app = FastAPI()
class Item(BaseModel):
nombre: str
precio: float
disponible: bool = True
tags: Optional[List[str]] = None
@app.post("/items/")
async def crear_item(item: Item) -> Item:
return item
@app.get("/items/{item_id}")
async def leer_item(item_id: int) -> dict:
return {"item_id": item_id}
Pydantic usa Type Hints para validar y convertir datos automáticamente. Más información en: Documentación Pydantic
Buenas Prácticas con Type Hints
1. Sé Gradual
No necesitas tipar todo de una vez. Comienza por las funciones públicas y APIs principales, luego expande gradualmente.
2. Prefiere Tipos Específicos
En lugar de Any, usa tipos más específicos siempre que sea posible. Any desactiva la verificación de tipos.
from typing import Any
Evita
def procesar(datos: Any) -> Any:
return datos
Prefiere
def procesar(datos: list[str]) -> list[str]:
return datos
3. Usa Optional Correctamente
Optional[X] es equivalente a Union[X, None]. Úsalo cuando el valor pueda ser None.
4. Configura tu Verificador de Tipos
Crea un archivo pyproject.toml o mypy.ini para configurar mypy:
# mypy.ini
[mypy]
python_version = 3.12
strict = True
ignore_missing_imports = True
5. Documenta Casos Complejos
Para tipos muy complejos, agrega comentarios o docstrings explicando la lógica.
Type Hints en Python 3.12 y 3.13
Las versiones más recientes de Python traen mejoras significativas al sistema de tipos:
- PEP 695 (Python 3.12): nueva sintaxis para TypeVar y Genéricos, más concisa.
- PEP 698 (Python 3.13): soporte mejorado para
typeen type hints. - PEP 649 (Python 3.14, previsto): evaluación diferida de anotaciones por defecto, reemplazando
from __future__ import annotations.
Fuente: PEP 695 - Type Parameter Syntax
Ejemplo Completo: Sistema de Pedidos
Ahora apliquemos todo lo aprendido en un ejemplo real:
from dataclasses import dataclass, field
from typing import List, Optional, Protocol, TypeVar
from datetime import datetime, date
from decimal import Decimal
T = TypeVar("T")
class Repositorio(Protocol[T]):
def guardar(self, entidad: T) -> None:
...
def buscar_por_id(self, id_: int) -> Optional[T]:
...
@dataclass
class Cliente:
id: int
nombre: str
email: str
fecha_registro: date = field(default_factory=date.today)
@dataclass
class ItemPedido:
producto: str
cantidad: int
precio_unitario: Decimal
@dataclass
class Pedido:
id: int
cliente: Cliente
items: List[ItemPedido]
fecha_creacion: datetime = field(default_factory=datetime.now)
estado: str = "pendiente"
def calcular_total(self) -> Decimal:
return sum(
item.cantidad * item.precio_unitario
for item in self.items
)
def actualizar_estado(self, nuevo_estado: str) -> None:
self.estado = nuevo_estado
class RepositorioCliente:
def guardar(self, cliente: Cliente) -> None:
print(f"Guardando cliente {cliente.nombre}")
def buscar_por_id(self, id_: int) -> Optional[Cliente]:
return Cliente(id=id_, nombre="Ana", email="[email protected]")
def procesar_pedido(
pedido: Pedido,
repositorio: Repositorio[Pedido]
) -> bool:
try:
repositorio.guardar(pedido)
pedido.actualizar_estado("procesado")
return True
except Exception:
return False
Uso
cliente = Cliente(id=1, nombre="María", email="[email protected]")
item = ItemPedido(producto="Notebook", cantidad=1, precio_unitario=Decimal("4500.00"))
pedido = Pedido(id=100, cliente=cliente, items=[item])
repo = RepositorioCliente()
print(f"Total del pedido: $ {pedido.calcular_total():.2f}")
procesar_pedido(pedido, repo) # type: ignore
Conclusión
Type Hints son una herramienta poderosa que hace que Python sea adecuado para proyectos de todos los tamaños. Mejoran la legibilidad del código, previenen errores, facilitan la refactorización y mejoran la experiencia de desarrollo con IDEs modernas.
Empieza a adoptar tipos gradualmente en tus proyectos — verás la diferencia en la calidad y mantenibilidad del código. Y recuerda: Type Hints son opcionales, ¡pero los beneficios que aportan son enormes!
Sigue aprendiendo con Universo Python. Explora más contenido sobre Data Classes, FastAPI y otros temas avanzados de Python.