Las bibliotecas que procesan anotaciones necesitan descubrir de qué objeto parte una expresión de tipo y qué argumentos contiene. En Python, typing.get_origin() y typing.get_args() son las herramientas públicas principales para esa introspección. Permiten analizar estructuras como list[int], dict[str, float], Annotated, Literal, uniones, alias y tipos genéricos sin depender de detalles internos frágiles.
Esta guía explica cómo interpretar orígenes y argumentos, construir validadores y generadores de schemas, manejar uniones y alias, conservar metadatos, reconocer límites y evitar errores comunes en frameworks que leen type hints en runtime.
El problema de la introspección de tipos
expresion = list[int]
print(expresion)
La representación es útil para una persona, pero comparar cadenas o acceder a atributos privados no es seguro. Los objetos internos de typing han cambiado entre versiones. Las funciones públicas ofrecen una interfaz soportada para separar el constructor de tipo de sus parámetros.
Primer ejemplo con get_origin
from typing import get_origin
expresion = list[int]
print(get_origin(expresion)) # <class 'list'>
El origin es el objeto base parametrizado. Para list[int], el origen es list. Para dict[str, int], es dict. En un tipo no parametrizado como int, normalmente se obtiene None.
Extraer argumentos con get_args
from typing import get_args
expresion = dict[str, list[int]]
print(get_args(expresion))
# (<class 'str'>, list[int])
get_args() devuelve una tupla con los parámetros. Esos argumentos pueden contener otros tipos parametrizados, por lo que las herramientas reales suelen recorrer la estructura de manera recursiva.
Inspector recursivo
from typing import get_args, get_origin
def describir(expresion: object, nivel: int = 0) -> None:
sangria = " " * nivel
origen = get_origin(expresion)
argumentos = get_args(expresion)
print(f"{sangria}tipo={expresion!r}, origen={origen!r}")
for argumento in argumentos:
describir(argumento, nivel + 1)
describir(dict[str, list[int | None]])
Este patrón es la base de serializadores, validadores, generadores de documentación e inyección de dependencias. Un algoritmo de producción debe tratar hojas sin argumentos y protegerse de recursión infinita en alias recursivos.
Uniones modernas
from types import UnionType
from typing import Union, get_args, get_origin
expresion = int | str
print(get_origin(expresion))
print(get_args(expresion))
Según la sintaxis y la versión de Python, el origen de una unión puede relacionarse con types.UnionType o typing.Union. No asumas una sola representación. Soporta las formas relevantes para la versión mínima del proyecto y prueba todos los intérpretes compatibles.
Optional es una unión
expresion = str | None
argumentos = get_args(expresion)
acepta_none = type(None) in argumentos
Optional[T] representa una unión de T y None. Buscar la palabra “Optional” en una representación textual es frágil. Analiza los argumentos y busca NoneType. Esto importa en validadores de configuración y APIs que distinguen campo ausente de valor nulo.
Literal
from typing import Literal, get_args, get_origin
Modo = Literal["lectura", "escritura"]
print(get_origin(Modo))
print(get_args(Modo))
En Literal, los argumentos son valores y no necesariamente tipos. Una herramienta genérica no puede asumir que cada elemento devuelto por get_args() es una clase. La guía sobre Literal en Python explica el uso de valores exactos.
Annotated y metadatos
from typing import Annotated, get_args, get_origin
Edad = Annotated[int, "mínimo 0", "máximo 130"]
print(get_origin(Edad))
print(get_args(Edad))
Los argumentos de Annotated empiezan con el tipo base y continúan con los metadatos. Los frameworks deben conservar el orden y decidir qué entradas reconocen. No descartes metadatos desconocidos si otra capa puede utilizarlos. Consulta Annotated en Python.
Callable requiere tratamiento especial
from collections.abc import Callable
from typing import get_args
TipoFuncion = Callable[[int, str], bool]
print(get_args(TipoFuncion))
La estructura de argumentos de Callable merece lógica dedicada. La lista de parámetros puede aparecer agrupada, y las formas con ParamSpec o Concatenate son más complejas. No trates Callable como una colección genérica ordinaria.
TypeVar y parámetros no resueltos
from typing import TypeVar
T = TypeVar("T")
Un TypeVar puede aparecer dentro de los argumentos y representar un parámetro todavía no sustituido. Un framework debe decidir si conserva el símbolo, aplica un mapa de especialización, usa su límite o rechaza schemas incompletos. Las restricciones también pueden afectar la interpretación.
Alias explícitos
type Resultado[T] = T | Exception
Los alias modernos pueden conservar identidad propia en runtime. Según la tarea, conviene mantener su nombre público o expandir el valor subyacente. La guía sobre TypeAliasType en Python explica por qué expandir automáticamente puede perder significado del dominio.
get_origin no sustituye get_type_hints
get_origin() y get_args() analizan un objeto de tipo ya disponible. No resuelven referencias futuras almacenadas como strings, anotaciones diferidas ni nombres dependientes de un namespace. Para obtener anotaciones resueltas de funciones y clases, usa typing.get_type_hints() con namespaces controlados.
from typing import get_type_hints
hints = get_type_hints(mi_funcion, include_extras=True)
Usa include_extras=True para conservar Annotated, Required, NotRequired y otros calificadores. La guía de get_type_hints en Python profundiza en resolución y seguridad.
Validador simplificado
from typing import get_args, get_origin
def validar(valor: object, expresion: object) -> bool:
origen = get_origin(expresion)
argumentos = get_args(expresion)
if origen is list:
if not isinstance(valor, list):
return False
(tipo_item,) = argumentos
return all(validar(item, tipo_item) for item in valor)
if origen is dict:
if not isinstance(valor, dict):
return False
tipo_clave, tipo_valor = argumentos
return all(
validar(clave, tipo_clave)
and validar(item, tipo_valor)
for clave, item in valor.items()
)
if origen is None and isinstance(expresion, type):
return isinstance(valor, expresion)
return False
El ejemplo demuestra la mecánica, pero no es un validador de producción. No cubre uniones, Literal, Annotated, TypedDict, Protocol, recursión, coerción ni mensajes detallados. Sí muestra cómo origin y args orientan el despacho.
TypedDict necesita una ruta propia
TypedDict es una clase especial de tipado, no un dict parametrizado común. Su estructura se obtiene de anotaciones y conjuntos de claves requeridas u opcionales. get_origin() no sustituye esa introspección. Consulta TypedDict en Python.
Protocol y runtime
Protocol también exige cuidado. Que un tipo tenga origen y argumentos no significa que pueda validarse con isinstance(). Los Protocol runtime-checkable realizan comprobaciones estructurales limitadas y no verifican firmas completas. No conviertas introspección estática en promesas que Python no garantiza.
Tipos no parametrizados
assert get_origin(int) is None
assert get_args(int) == ()
Una tupla vacía no indica necesariamente error. Puede representar un tipo simple, un alias no expandido u otra forma especial. El consumidor debe interpretar el contexto.
Orden y normalización
El orden de argumentos suele ser significativo, pero caches internos y normalización de uniones pueden producir objetos equivalentes con historias diferentes. No uses repr() como clave persistente. Crea una representación canónica y versionada si necesitas almacenar schemas.
Seguridad
Las funciones de introspección solo examinan objetos, pero suelen utilizarse después de get_type_hints(), que puede evaluar referencias. No proceses anotaciones de código no confiable sin aislamiento. Los sistemas de plugins deben limitar módulos y namespaces permitidos.
Compatibilidad entre versiones
Prueba el comportamiento en cada versión soportada. El sistema de tipos incorpora alias explícitos, genéricos variádicos, calificadores y mecanismos de evaluación nuevos. Prefiere APIs públicas, evita clases internas con nombres privados y centraliza la compatibilidad.
Errores comunes
- Comparar reprs: las representaciones textuales no son contratos estables.
- Suponer que args son tipos: Literal devuelve valores y Annotated incluye metadatos.
- Ignorar alias: expandirlos siempre puede perder nombres públicos.
- Tratar cada origen como clase: las formas especiales necesitan despacho propio.
- Olvidar referencias futuras: resuelve hints antes del análisis cuando sea necesario.
- Sobreprometer Protocol: la presencia de atributos no prueba firmas.
Arquitectura recomendada
Separa resolución, normalización y consumo. Primero obtén hints con namespaces controlados. Después normaliza origins, argumentos, alias y metadatos en un árbol intermedio. Finalmente usa ese árbol para validación, documentación o serialización. Esta separación reduce el acoplamiento con detalles de typing y facilita las pruebas.
Conclusión
typing.get_origin() y typing.get_args() son la base pública para desmontar tipos parametrizados. Permiten construir frameworks robustos, pero uniones, Literal, Annotated, Callable, alias, TypeVar, TypedDict y Protocol requieren tratamiento consciente.
La documentación oficial de get_origin y get_args define la API. Combínala con get_type_hints(), conserva metadatos y mantén una capa de compatibilidad probada en todas las versiones soportadas.







