En esta página

Decoradores y type hints

12 min lectura TextoCap. 5 — Python para web

Type hints — El sistema de tipos de Python

Los type hints (anotaciones de tipo) son la forma de documentar e indicar los tipos esperados de variables, parámetros y valores de retorno en Python. No afectan la ejecución del programa, pero son invaluables para:

  • Detectar errores con herramientas como mypy o pyright antes de ejecutar
  • Mejorar el autocompletado en el editor (VS Code, PyCharm)
  • Documentar el código de forma más precisa
  • Habilitar generación automática de documentación (FastAPI usa type hints para sus esquemas)

Tipos básicos y colecciones

# Tipos primitivos
nombre: str = "Ana"
edad: int = 28
altura: float = 1.68
activo: bool = True

# Anotaciones sin asignación (en clases, esto define campos)
class Usuario:
    id: int
    nombre: str
    email: str


# Colecciones modernas (Python 3.9+)
# Antes: from typing import List, Dict, Tuple, Set
# Ahora: usa directamente list[], dict[], tuple[], set[]
numeros: list[int] = [1, 2, 3, 4, 5]
configuracion: dict[str, str] = {"host": "localhost", "puerto": "8080"}
coordenadas: tuple[float, float] = (40.4168, -3.7038)
etiquetas: set[str] = {"python", "backend", "api"}

# Tipos opcionales (Python 3.10+)
# Antes: from typing import Optional; Optional[str] → str | None
resultado: str | None = None
nombre_usuario: str | None = "ana_garcia"

# Uniones (Python 3.10+)
# Antes: from typing import Union; Union[int, str]
identificador: int | str = 42
valor: int | float | str = 3.14

El módulo typing — Tipos avanzados

from typing import (
    Callable,
    TypeVar,
    Generic,
    Protocol,
    TypedDict,
    Literal,
    Final,
    ClassVar,
    Annotated,
    Any,
    Never,
    Self,
)

# Callable — para funciones como argumentos
def aplicar_transformacion(
    datos: list[int],
    transformacion: Callable[[int], int]
) -> list[int]:
    return [transformacion(x) for x in datos]

resultado = aplicar_transformacion([1, 2, 3], lambda x: x ** 2)

# Callable con tipos de retorno complejos
ProcesadorTexto = Callable[[str, bool], str]

# Literal — solo acepta valores específicos
def configurar_modo(modo: Literal["debug", "producción", "prueba"]) -> None:
    print(f"Modo configurado: {modo}")

configurar_modo("debug")      # OK
# configurar_modo("otro")     # mypy lanza error

# Final — constantes que no pueden reasignarse
MAX_REINTENTOS: Final = 3
VERSION: Final[str] = "1.0.0"

# ClassVar — atributos de clase (no de instancia)
class Singleton:
    _instancia: ClassVar["Singleton | None"] = None

    @classmethod
    def obtener_instancia(cls) -> "Singleton":
        if cls._instancia is None:
            cls._instancia = cls()
        return cls._instancia


# TypedDict — diccionarios con estructura específica
class ConfigDB(TypedDict):
    host: str
    puerto: int
    nombre: str
    usuario: str
    contraseña: str

def conectar(config: ConfigDB) -> None:
    print(f"Conectando a {config['host']}:{config['puerto']}/{config['nombre']}")

config: ConfigDB = {
    "host": "localhost",
    "puerto": 5432,
    "nombre": "mi_db",
    "usuario": "admin",
    "contraseña": "secreto"
}

TypeVar y Genéricos

from typing import TypeVar, Generic

T = TypeVar("T")
K = TypeVar("K")
V = TypeVar("V")

# Función genérica
def primero(lista: list[T]) -> T | None:
    return lista[0] if lista else None

print(primero([1, 2, 3]))      # 1 (inferido como int)
print(primero(["a", "b"]))     # 'a' (inferido como str)
print(primero([]))             # None

# Clase genérica
class Pila(Generic[T]):
    """Estructura de datos pila (LIFO) genérica."""

    def __init__(self) -> None:
        self._items: list[T] = []

    def apilar(self, item: T) -> None:
        self._items.append(item)

    def desapilar(self) -> T:
        if not self._items:
            raise IndexError("La pila está vacía")
        return self._items.pop()

    def ver_tope(self) -> T | None:
        return self._items[-1] if self._items else None

    def __len__(self) -> int:
        return len(self._items)

    def __bool__(self) -> bool:
        return bool(self._items)


pila_int: Pila[int] = Pila()
pila_int.apilar(1)
pila_int.apilar(2)
pila_int.apilar(3)
print(pila_int.desapilar())  # 3

pila_str: Pila[str] = Pila()
pila_str.apilar("Python")
pila_str.apilar("FastAPI")

Anotaciones de retorno especiales

from typing import Never, NoReturn

def lanzar_error(mensaje: str) -> Never:
    """Función que SIEMPRE lanza una excepción."""
    raise ValueError(mensaje)

def salir_programa(codigo: int = 0) -> NoReturn:
    """Función que nunca retorna normalmente."""
    import sys
    sys.exit(codigo)

# Self — para métodos que devuelven la instancia (Python 3.11+)
from typing import Self

class Constructor:
    def __init__(self) -> None:
        self._datos: dict[str, str] = {}

    def agregar(self, clave: str, valor: str) -> Self:
        self._datos[clave] = valor
        return self  # Permite encadenamiento

    def construir(self) -> dict[str, str]:
        return self._datos.copy()

resultado = (Constructor()
             .agregar("nombre", "Ana")
             .agregar("rol", "admin")
             .construir())

Decoradores en Python

Un decorador es una función que recibe otra función y devuelve una versión modificada de ella. Es un patrón de diseño poderoso para agregar comportamiento transversal (logging, caché, validación, autenticación) sin modificar el código original.

Decorador básico

import time
import functools
from typing import Callable, TypeVar, Any

F = TypeVar("F", bound=Callable[..., Any])

def cronometrar(func: F) -> F:
    """Mide y muestra el tiempo de ejecución de una función."""
    @functools.wraps(func)
    def envoltura(*args: Any, **kwargs: Any) -> Any:
        inicio = time.perf_counter()
        try:
            resultado = func(*args, **kwargs)
            return resultado
        finally:
            duracion = time.perf_counter() - inicio
            print(f"[Tiempo] {func.__name__}: {duracion:.4f}s")

    return envoltura  # type: ignore[return-value]


@cronometrar
def ordenar_grande(n: int) -> list[int]:
    """Genera y ordena una lista grande de números."""
    import random
    datos = [random.randint(0, n) for _ in range(n)]
    return sorted(datos)

resultado = ordenar_grande(100_000)
print(f"Primer elemento: {resultado[0]}")
# [Tiempo] ordenar_grande: 0.0523s

Decorador con argumentos

Para crear un decorador que acepte argumentos, necesitas un nivel extra de función:

import logging
from functools import wraps
from typing import Callable, TypeVar, Any

F = TypeVar("F", bound=Callable[..., Any])

def registrar(nivel: str = "INFO", incluir_args: bool = False):
    """Decorador configurable que registra llamadas a funciones."""
    nivel_log = getattr(logging, nivel.upper(), logging.INFO)

    def decorador(func: F) -> F:
        @wraps(func)
        def envoltura(*args: Any, **kwargs: Any) -> Any:
            if incluir_args:
                logging.log(nivel_log, f"Llamando {func.__name__}({args}, {kwargs})")
            else:
                logging.log(nivel_log, f"Llamando {func.__name__}")

            try:
                resultado = func(*args, **kwargs)
                logging.log(nivel_log, f"{func.__name__} completó exitosamente")
                return resultado
            except Exception as e:
                logging.error(f"{func.__name__} lanzó {type(e).__name__}: {e}")
                raise

        return envoltura  # type: ignore[return-value]
    return decorador


@registrar(nivel="DEBUG", incluir_args=True)
def calcular_potencia(base: float, exponente: float) -> float:
    return base ** exponente

@registrar(nivel="INFO")
def procesar_pagos(mes: str) -> int:
    return 42  # Número de pagos procesados

Caché con decoradores

import functools
from typing import Callable, TypeVar, Any

# lru_cache — memoización integrada
from functools import lru_cache, cache

@cache  # Equivalente a @lru_cache(maxsize=None)
def fibonacci(n: int) -> int:
    """Fibonacci con memoización automática."""
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(50))  # Muy rápido gracias a la caché
print(fibonacci.cache_info())  # CacheInfo(hits=48, misses=51, ...)

@lru_cache(maxsize=128)  # Caché con límite de 128 entradas
def buscar_usuario_db(user_id: int) -> dict:
    """Simula búsqueda en base de datos con caché."""
    print(f"Consultando BD para usuario {user_id}...")
    return {"id": user_id, "nombre": f"Usuario {user_id}"}

# Decorador de caché personalizado con TTL
def caché_con_ttl(segundos: float):
    """Caché que expira después de N segundos."""
    def decorador(func: F) -> F:
        cache_interna: dict[tuple, tuple] = {}

        @wraps(func)
        def envoltura(*args: Any, **kwargs: Any) -> Any:
            clave = (args, tuple(sorted(kwargs.items())))
            ahora = time.time()

            if clave in cache_interna:
                valor, timestamp = cache_interna[clave]
                if ahora - timestamp < segundos:
                    return valor

            resultado = func(*args, **kwargs)
            cache_interna[clave] = (resultado, ahora)
            return resultado

        return envoltura  # type: ignore[return-value]
    return decorador


@caché_con_ttl(segundos=60)
def obtener_tasa_cambio(moneda: str) -> float:
    """Caché de 60 segundos para tasas de cambio."""
    print(f"Consultando tasa de {moneda}...")
    return 1.08 if moneda == "EUR" else 0.79  # Simulado

Decoradores apilados

Los decoradores se aplican de abajo hacia arriba:

@registrar(nivel="INFO")
@cronometrar
@caché_con_ttl(segundos=30)
def proceso_costoso(datos: list[int]) -> int:
    """Función con múltiples decoradores apilados."""
    time.sleep(0.1)  # Simula trabajo pesado
    return sum(datos)

# Equivalente a:
# proceso_costoso = registrar(nivel="INFO")(cronometrar(caché_con_ttl(30)(proceso_costoso)))

resultado = proceso_costoso([1, 2, 3, 4, 5])

Decoradores de clase

from typing import TypeVar, Type

C = TypeVar("C", bound=type)

def singleton(cls: C) -> C:
    """Decorador que convierte una clase en singleton."""
    instancias: dict[C, C] = {}

    @wraps(cls)
    def obtener_instancia(*args: Any, **kwargs: Any) -> C:
        if cls not in instancias:
            instancias[cls] = cls(*args, **kwargs)  # type: ignore
        return instancias[cls]

    return obtener_instancia  # type: ignore[return-value]


@singleton
class ConexiónBD:
    def __init__(self, url: str = "sqlite:///app.db") -> None:
        self.url = url
        print(f"Conectando a {url}")

bd1 = ConexiónBD()
bd2 = ConexiónBD()
print(bd1 is bd2)  # True — es el mismo objeto

Verificación de tipos con mypy

# Instalar
pip install mypy

# Verificar un archivo
mypy mi_archivo.py

# Verificación estricta
mypy --strict mi_archivo.py

# Verificar un proyecto completo
mypy src/

# Configurar en pyproject.toml
# [tool.mypy]
# strict = true
# python_version = "3.14"

Resumen

Los type hints transforman Python en un lenguaje con tipado opcional pero poderoso. Los TypeVar y Generic permiten escribir código reutilizable con seguridad de tipos. Los decoradores son una forma elegante de agregar comportamiento transversal sin modificar funciones existentes. @functools.wraps es obligatorio para preservar la identidad de la función decorada. En la próxima y última lección pondrás todo en práctica construyendo un proyecto completo.

@wraps preserva la identidad de la función decorada
Sin @functools.wraps, el decorador 'consume' el __name__, __doc__ y __module__ de la función original. @wraps los copia a la función envolvente, haciendo que el debugging, la documentación automática y las herramientas de introspección funcionen correctamente.
Usa mypy para verificación estática de tipos
Instala mypy con 'pip install mypy' y ejecuta 'mypy tu_archivo.py'. Te mostrará errores de tipo antes de ejecutar el código, similar a TypeScript pero para Python. Para mayor rigor usa 'mypy --strict'.
Los type hints no se verifican en tiempo de ejecución por defecto
Python ignora los type hints durante la ejecución. Son para herramientas de análisis estático (mypy, pyright) y para los desarrolladores que leen el código. Si necesitas validación en runtime, usa Pydantic o beartype.