Bibliotecas que processam anotações precisam descobrir de que tipo uma expressão foi construída e quais argumentos ela contém. Em Python, typing.get_origin() e typing.get_args() são as ferramentas centrais para essa introspecção. Elas permitem analisar estruturas como list[int], dict[str, float], Annotated, Literal, uniões, aliases e tipos genéricos sem depender de detalhes internos frágeis.
Neste guia, você aprenderá a interpretar origins e argumentos, criar validadores e geradores de schema, lidar com uniões e aliases, preservar metadados, reconhecer limitações e evitar erros comuns em frameworks que leem type hints em runtime.
O problema da introspecção de tipos
tipo = list[int]
print(tipo)
A representação mostra informação útil, mas comparar strings ou acessar atributos privados é inseguro. A implementação interna de objetos de tipagem pode mudar entre versões. As funções públicas de typing fornecem uma interface estável para separar o construtor do tipo de seus parâmetros.
Primeiro exemplo com get_origin
from typing import get_origin
tipo = list[int]
print(get_origin(tipo)) # <class 'list'>
O origin é o objeto base que foi parametrizado. Para list[int], o origin é list. Para dict[str, int], é dict. Em tipos não parametrizados, como int, o resultado normalmente é None.
Extraindo argumentos com get_args
from typing import get_args
tipo = dict[str, list[int]]
print(get_args(tipo))
# (<class 'str'>, list[int])
get_args() devolve uma tupla com os parâmetros de tipo. Os argumentos podem conter outros tipos parametrizados, portanto ferramentas reais geralmente percorrem a estrutura recursivamente.
Construindo um inspetor recursivo
from typing import get_args, get_origin
def descrever(tipo: object, nivel: int = 0) -> None:
recuo = " " * nivel
origem = get_origin(tipo)
argumentos = get_args(tipo)
print(f"{recuo}tipo={tipo!r}, origem={origem!r}")
for argumento in argumentos:
descrever(argumento, nivel + 1)
descrever(dict[str, list[int | None]])
Esse padrão é a base de serializadores, validadores, geradores de documentação e ferramentas de injeção de dependência. O algoritmo precisa tratar folhas sem argumentos e evitar recursão infinita em aliases recursivos.
Uniões modernas
from types import UnionType
from typing import Union, get_args, get_origin
tipo = int | str
print(get_origin(tipo))
print(get_args(tipo))
Dependendo da forma e da versão do Python, a origem de uma união pode estar relacionada a types.UnionType ou typing.Union. Em vez de assumir uma única representação, compare com as formas suportadas pela versão mínima do projeto e mantenha testes em todos os interpretadores compatíveis.
Optional é uma união
tipo = str | None
argumentos = get_args(tipo)
aceita_none = type(None) in argumentos
Optional[T] representa uma união entre T e None. Detectar apenas o nome “Optional” é frágil. Analise os argumentos e procure NoneType. Esse cuidado é importante em validadores de configuração e APIs que distinguem campo ausente de valor nulo.
Literal
from typing import Literal, get_args, get_origin
Modo = Literal["leitura", "escrita"]
print(get_origin(Modo))
print(get_args(Modo))
Em Literal, os argumentos são valores, não necessariamente tipos. Uma ferramenta genérica não pode presumir que tudo retornado por get_args() seja uma classe. O guia sobre Literal no Python mostra como valores exatos melhoram contratos e overloads.
Annotated e metadados
from typing import Annotated, get_args, get_origin
Idade = Annotated[int, "mínimo 0", "máximo 130"]
print(get_origin(Idade))
print(get_args(Idade))
Os argumentos de Annotated começam pelo tipo base e continuam com os metadados. Frameworks devem preservar a ordem e decidir quais metadados reconhecem. Não descarte informações desconhecidas silenciosamente quando outra camada puder utilizá-las. Consulte também Annotated no Python.
Callable
from collections.abc import Callable
from typing import get_args
TipoFuncao = Callable[[int, str], bool]
print(get_args(TipoFuncao))
A estrutura dos argumentos de Callable merece tratamento específico. A lista de parâmetros pode aparecer agrupada, e formas com ParamSpec ou Concatenate são mais complexas. Não trate Callable como uma coleção genérica comum.
TypeVar e parâmetros genéricos
from typing import TypeVar
T = TypeVar("T")
Um TypeVar pode surgir dentro dos argumentos e representar uma variável ainda não substituída. Ferramentas devem decidir se mantêm o parâmetro simbólico, aplicam um mapeamento de especialização ou rejeitam schemas incompletos. Limites e restrições de TypeVar também podem influenciar a interpretação.
Aliases explícitos
type Resultado[T] = T | Exception
Aliases modernos podem preservar identidade própria em runtime. Dependendo da tarefa, você pode querer manter o nome público do alias ou expandir seu valor. O artigo sobre TypeAliasType no Python explica como aliases explícitos se comportam e por que expansão automática pode perder semântica.
get_origin não substitui get_type_hints
get_origin() e get_args() analisam um objeto de tipo já disponível. Eles não resolvem automaticamente referências futuras em strings, namespaces ou anotações adiadas. Para obter anotações resolvidas de funções e classes, use typing.get_type_hints() com os namespaces apropriados.
from typing import get_type_hints
hints = get_type_hints(minha_funcao, include_extras=True)
Use include_extras=True quando precisar preservar Annotated, Required, NotRequired e outros qualificadores. O guia sobre get_type_hints no Python aprofunda resolução e segurança.
Exemplo: validador simplificado
from typing import get_args, get_origin
def validar(valor: object, tipo: object) -> bool:
origem = get_origin(tipo)
argumentos = get_args(tipo)
if origem is list:
if not isinstance(valor, list):
return False
(tipo_item,) = argumentos
return all(validar(item, tipo_item) for item in valor)
if origem is dict:
if not isinstance(valor, dict):
return False
tipo_chave, tipo_valor = argumentos
return all(
validar(chave, tipo_chave)
and validar(item, tipo_valor)
for chave, item in valor.items()
)
if origem is None and isinstance(tipo, type):
return isinstance(valor, tipo)
return False
O exemplo demonstra a mecânica, mas não é um validador de produção. Ele não cobre uniões, Literal, Annotated, TypedDict, Protocol, recursão, coerção, mensagens detalhadas nem tipos especiais. Ainda assim, mostra como origin e args orientam o despacho.
TypedDict
TypedDict é uma classe especial de tipagem, não um dict parametrizado comum. Sua estrutura é encontrada em __annotations__, __required_keys__ e __optional_keys__. get_origin() não substitui essa introspecção específica. O guia sobre TypedDict no Python explica esses contratos.
Protocol
Protocol também exige tratamento especializado. O fato de um objeto possuir origin e argumentos não prova que ele possa ser validado com isinstance(). Protocols runtime-checkable verificam apenas presença estrutural limitada, não assinaturas completas. Evite transformar introspecção estática em promessas de runtime que o Python não garante.
Tipos não parametrizados
assert get_origin(int) is None
assert get_args(int) == ()
Uma tupla vazia de argumentos não significa erro. Ela pode indicar um tipo simples, um alias não expandido ou uma forma especial. O chamador deve interpretar o contexto.
Ordem e normalização
A ordem dos argumentos costuma ser significativa, mas caches internos e normalização de uniões podem alterar representações equivalentes. Não use repr() como chave persistente de schema. Crie uma representação canônica própria e versionada quando precisar armazenar resultados.
Segurança
As funções de introspecção em si apenas examinam objetos, mas normalmente são usadas depois de get_type_hints(), que pode avaliar referências. Não processe anotações de código não confiável sem isolamento. Bibliotecas de plugins devem definir claramente quais módulos e namespaces podem ser carregados.
Compatibilidade
Teste o comportamento em cada versão suportada do Python. A evolução do sistema de tipos introduz novas formas, como aliases explícitos, genéricos variádicos e qualificadores adicionais. Prefira APIs públicas, evite classes internas com nomes iniciados por sublinhado e mantenha uma camada de compatibilidade centralizada.
Erros comuns
- Comparar reprs: representações textuais não são contratos estáveis.
- Assumir que args são tipos: Literal devolve valores e Annotated inclui metadados.
- Ignorar aliases: expandi-los sempre pode perder nomes públicos.
- Tratar toda origem como classe: formas especiais exigem despacho próprio.
- Esquecer referências futuras: resolva hints antes da análise quando necessário.
- Validar Protocol superficialmente: presença de atributos não garante assinatura.
Arquitetura recomendada
Separe resolução, normalização e consumo. Primeiro obtenha hints com namespaces controlados. Depois normalize origins, args, aliases e metadados em uma árvore intermediária. Por fim, use essa árvore para validação, documentação ou serialização. Essa separação reduz acoplamento com detalhes de typing e facilita testes.
Conclusão
typing.get_origin() e typing.get_args() fornecem a base pública para desmontar tipos parametrizados. Elas ajudam a construir frameworks robustos, mas exigem tratamento consciente para uniões, Literal, Annotated, Callable, aliases, TypeVar, TypedDict e Protocol.
A documentação oficial de get_origin e get_args descreve a API. Use-a junto de get_type_hints(), preserve metadados e mantenha uma camada de compatibilidade testada em todas as versões suportadas.







