Cuando escribes una función que simplemente devuelve lo que recibe, el tipado ingenuo te traiciona. Si declaras def identidad(x: Any) -> Any, el type checker pierde la pista: entra un int, devuelve Any, y toda inferencia aguas abajo se rompe. Si declaras -> int, la función deja de ser genérica. El problema es que necesitas expresar una relación: el tipo de salida es el mismo que el de entrada, sea cual sea.
Eso es exactamente lo que resuelve TypeVar: una variable de tipo que representa un tipo concreto pero desconocido en el momento de la definición. Cuando el type checker analiza una llamada concreta, instancia ese TypeVar con el tipo real y verifica la consistencia. No es Any —que corta la inferencia— sino un placeholder con memoria.
Hay tres sabores importantes. El TypeVar sin restricciones acepta cualquier tipo y solo garantiza que entrada y salida coincidan. El TypeVar con restricciones (TypeVar('T', int, str)) limita los tipos permitidos a una lista cerrada —útil cuando la implementación solo tiene sentido para ciertos tipos. El TypeVar con bound (TypeVar('T', bound=Comparable)) acepta cualquier subtipo de una clase base, preservando el tipo concreto en la firma —más flexible que restricciones cuando modelas jerarquías.
Si usas Python 3.12+, la sintaxis PEP 695 elimina el boilerplate: en vez de declarar el TypeVar por separado, lo introduces directamente entre corchetes en la firma. El resultado es más legible y el compilador lo trata exactamente igual.
El riesgo de hacerlo mal es sutil: confundir TypeVar con Union. Union[int, str] significa “puede ser int o str, y no sé cuál”. TypeVar con restricciones significa “en esta invocación concreta es uno de ellos, y el checker lo recuerda”. Si usas Union donde necesitas TypeVar, pierdes la correlación entre argumentos y valor de retorno.
from __future__ import annotations
from typing import TypeVar, Generic
# ── TypeVar sin restricciones ────────────────────────────────────────────
T = TypeVar("T")
def identity(x: T) -> T:
return x
reveal_type(identity(42)) # int, no Any
reveal_type(identity("hello")) # str
# ── TypeVar con restricciones ────────────────────────────────────────────
Numeric = TypeVar("Numeric", int, float)
def double(x: Numeric) -> Numeric:
return x * 2 # type: ignore[operator] # válido para int y float
# ── TypeVar con bound ────────────────────────────────────────────────────
from collections.abc import Sequence
S = TypeVar("S", bound=Sequence)
def first(seq: S) -> S:
# Devuelve el mismo subtipo de Sequence, no Sequence genérico
return seq[:1] # type: ignore[return-value]
reveal_type(first([1, 2, 3])) # list[int]
reveal_type(first("abcdef")) # str
# ── Clase genérica con Generic[T] ────────────────────────────────────────
class Stack(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()
def peek(self) -> T:
return self._items[-1]
int_stack: Stack[int] = Stack()
int_stack.push(10)
int_stack.push(20)
value: int = int_stack.pop() # el checker sabe que es int
# ── PEP 695 (Python 3.12+): sintaxis nativa ──────────────────────────────
# No necesitas importar TypeVar ni Generic
def identity_312[T](x: T) -> T: # T se declara inline
return x
class Stack312[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()
Desglose del código
T = TypeVar("T") crea un objeto que el type checker trata como “cualquier tipo, pero el mismo en toda esta invocación”. La cadena "T" debe coincidir con el nombre de la variable —convenio estricto que herramientas como mypy exigen para evitar confusión.
En double, el TypeVar Numeric con restricciones (int, float) hace algo más preciso que Union: si llamas double(3), el checker instancia Numeric = int y confirma que la operación * 2 es válida para int. No hay ambigüedad sobre el tipo de retorno.
El TypeVar con bound=Sequence en first es más potente que las restricciones cuando el conjunto de tipos válidos es abierto. Cualquier subclase de Sequence —list, str, tuple, tu propia colección— es válida, y el tipo concreto se preserva en el retorno. Si hubieras puesto -> Sequence, perderías información: el checker no sabría que first([1,2,3]) devuelve un list.
Stack(Generic[T]) demuestra cómo trasladar el mismo mecanismo a clases. Al anotar _items: list[T], todos los métodos que usan T quedan ligados al tipo con el que instanciaste el stack. El checker rechazará int_stack.push("texto") sin necesidad de ninguna lógica de validación en runtime.
La sintaxis PEP 695 no añade funcionalidad nueva —es azúcar sintáctica que reduce el ruido. La semántica es idéntica: [T] en la firma es equivalente a declarar T = TypeVar("T") y heredar de Generic[T]. En proyectos que ya usan 3.12+, es la forma preferida.
Errores que debes conocer
Error: Usar el mismo TypeVar en dos funciones independientes como si fueran distintos, cuando en realidad comparten el mismo objeto y el checker puede correlacionarlos inesperadamente.
# ❌ Wrong
T = TypeVar("T")
def first(x: T) -> T: ...
def second(y: T) -> T: ... # mismo T, pero son funciones sin relación
# ✅ Right
T1 = TypeVar("T1")
T2 = TypeVar("T2")
def first(x: T1) -> T1: ...
def second(y: T2) -> T2: ...
Usar TypeVars distintos evita que el checker asuma correlación entre llamadas a funciones que no tienen ninguna relación entre sí.
Error: Confundir TypeVar con restricciones y Union en el tipo de retorno, perdiendo la correlación.
# ❌ Wrong
from typing import Union
def parse(x: Union[int, str]) -> Union[int, str]:
return x
# el checker no sabe si parse(42) devuelve int o str
# ✅ Right
from typing import TypeVar
T = TypeVar("T", int, str)
def parse(x: T) -> T:
return x
# parse(42) -> int, parse("hi") -> str: correlación preservada
Con TypeVar, el checker instancia el tipo en cada llamada y propaga esa información; con Union solo ve “uno de los dos, desconocido”.
Error: Definir un TypeVar sin pasar el nombre como string literal coincidente con la variable, lo que rompe la introspección y causa warnings en mypy.
# ❌ Wrong
T = TypeVar("MyType") # el nombre interno no coincide con la variable
# ✅ Right
T = TypeVar("T") # el string debe ser idéntico al nombre de la variable
Mypy y pyright usan ese string para mensajes de error y para la consistencia del protocolo __class_getitem__; no es solo una convención, tiene efecto observable en el diagnóstico.
N° 127