Un genérico preserva relaciones entre tipos sin duplicar implementaciones. Si una función recibe un valor y devuelve el mismo tipo, TypeVar comunica mejor el contrato que Any.

Funciones y clases genéricas

from typing import Generic, TypeVar

T = TypeVar("T")


def primero(elementos: list[T]) -> T:
    if not elementos:
        raise ValueError("la lista no puede estar vacía")
    return elementos[0]


class Caja(Generic[T]):
    def __init__(self, valor: T) -> None:
        self.valor = valor

    def obtener(self) -> T:
        return self.valor

Un verificador infiere str para primero(["a"]) y Caja("a").obtener(). Proyectos limitados a Python 3.12 o posterior pueden usar def primero[T](...); respeta la compatibilidad declarada.

Bounds y constraints

Un bound acepta subtipos de un límite; las constraints restringen alternativas exactas. No añadas varianza manual sin comprender si el tipo se lee, se escribe o ambas cosas. Empieza invariante.

La relación que conserva TypeVar

Compara estas firmas:

from typing import Any, TypeVar

T = TypeVar("T")


def identidad_debil(valor: Any) -> Any:
    return valor


def identidad(valor: T) -> T:
    return valor

Con Any, un verificador permite casi cualquier operación sobre el resultado. Con T, relaciona entrada y salida. identidad("python") es str e identidad(42) es int. Esto no crea un tipo en ejecución ni convierte valores; proporciona una variable que el analizador resuelve en cada llamada.

Una variable de tipo usada una sola vez normalmente no aporta información. def registrar(valor: T) -> None suele poder aceptar object, pues no hay otra posición relacionada. Usa TypeVar cuando el tipo reaparece en el retorno, otro parámetro o el estado de una clase.

Varios parámetros y colecciones

Los genéricos relacionan entrada, callable y resultado:

from collections.abc import Callable, Iterable

Entrada = TypeVar("Entrada")
Salida = TypeVar("Salida")


def transformar(
    elementos: Iterable[Entrada],
    funcion: Callable[[Entrada], Salida],
) -> list[Salida]:
    return [funcion(elemento) for elemento in elementos]

Para transformar(["1", "20"], int), un checker infiere list[int]. La función acepta cualquier iterable y no solo listas porque únicamente necesita iterar. Elegir el protocolo de entrada más limitado mejora la reutilización sin perder precisión.

Bounds para capacidades compartidas

Un límite superior admite cualquier subtipo que satisfaga la base indicada:

from typing import Protocol


class Serializable(Protocol):
    def a_dict(self) -> dict[str, object]:
        ...


S = TypeVar("S", bound=Serializable)


def mantener_original(elemento: S) -> S:
    elemento.a_dict()
    return elemento

El cuerpo puede llamar a a_dict, y el retorno conserva el tipo concreto del elemento. Devolver solo Serializable perdería esa información. Un bound expresa una capacidad mínima, no una conversión.

Las constraints son diferentes: Texto = TypeVar("Texto", str, bytes) acepta esas familias y resuelve el resultado como una alternativa listada. Un subtipo específico de str se promueve a str. Úsalas si la implementación admite un conjunto cerrado y maneja cada alternativa coherentemente. Para una capacidad abierta, prefiere bound o Protocol.

Clases genéricas y encapsulación

Una clase genérica mantiene el parámetro entre operaciones:

class Pila(Generic[T]):
    def __init__(self) -> None:
        self._elementos: list[T] = []

    def agregar(self, elemento: T) -> None:
        self._elementos.append(elemento)

    def retirar(self) -> T:
        if not self._elementos:
            raise IndexError("pila vacía")
        return self._elementos.pop()

Pila[str] acepta cadenas y devuelve str. La parametrización mejora el autocompletado y detecta usos incoherentes antes de ejecutar. Sin embargo, no impide pila.agregar(3) en runtime. Valida explícitamente JSON, formularios o datos de red en la frontera.

Varianza sin adivinar

Un contenedor que lee y escribe T normalmente es invariante. Aunque Perro sea subtipo de Animal, tratar Pila[Perro] como Pila[Animal] sería inseguro: alguien podría agregar un Gato. Una interfaz de solo lectura puede ser covariante; un consumidor solo de entrada puede ser contravariante.

No declares varianza solo para silenciar un error. Examina las operaciones públicas y comprueba si el parámetro aparece en entrada, salida o ambas. La sintaxis moderna permite que verificadores infieran varianza de clases; confirma el soporte de Python y herramientas del proyecto.

Sintaxis de Python 3.12

PEP 695 introdujo parámetros entre corchetes:

def primero[T](elementos: list[T]) -> T:
    return elementos[0]


class Caja[T]:
    def __init__(self, valor: T) -> None:
        self.valor = valor

Esta forma evita declaraciones globales, pero intérpretes antiguos ni siquiera pueden analizar el archivo. Las bibliotecas deben respetar su versión mínima. No mezcles estilos sin razón y revisa la versión configurada de mypy, Pyright u otro analizador.

Errores y criterios prácticos

Evita devolver Any desde una función genérica porque rompe la relación que querías conservar. Tampoco prometas T sin recibirlo o construirlo de forma segura; el checker no sabe qué valor concreto crear. Los casts pueden ocultar errores de diseño y deben ser excepcionales y explicados.

Empieza con ejemplos concretos de llamadas. Si dos posiciones deben mantener el mismo tipo o un resultado depende del callable recibido, un genérico probablemente ayuda. Si la función acepta valores heterogéneos y devuelve información independiente, object, una unión o un protocolo puede ser más honesto.

Inferencia, tipos explícitos y diagnóstico

Deja que el verificador infiera parámetros en la mayoría de llamadas. Escribe caja: Caja[str] = Caja("texto") cuando la anotación documente una frontera, resuelva una colección inicialmente vacía o evite una inferencia demasiado amplia. Repetir tipos obvios en cada variable añade ruido.

Si el checker da un resultado inesperado, reduce el ejemplo e inspecciona el tipo inferido con una función como reveal_type. Revisa cada aparición de T: los parámetros pueden imponer requisitos distintos y hacer que el analizador elija un ancestro común. Una función que recibe dos valores del mismo T no garantiza clases idénticas en runtime, pues puede existir un tipo compartido válido.

Los aliases genéricos hacen legibles estructuras largas. Python 3.12 admite type Resultado[T] = tuple[T, str | None]. En versiones anteriores, usa recursos compatibles de typing. Un alias nombra una forma de datos, pero no crea validación ni un tipo nominal distinto.

Ejecuta el verificador en CI con configuración versionada. Endurece reglas gradualmente en vez de tapar errores con Any. Los genéricos aportan valor cuando sus contratos se analizan continuamente y siguen siendo comprensibles.

Revisa type hints en Python y mypy. La documentación oficial de typing, consultada el 22 de julio de 2026, cubre TypeVar, genéricos y varianza. Usa genéricos para relaciones reales, no para complicar una API sencilla.