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 type en 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.