Anotações de tipo normalmente descrevem apenas quais valores uma função aceita ou devolve. Em aplicações reais, porém, também precisamos comunicar restrições, unidades, formatos, documentação, regras de validação e informações consumidas por frameworks. typing.Annotated permite associar metadados a um tipo sem alterar sua identidade básica para verificadores que não entendem esses metadados.
Neste guia, você aprenderá a criar tipos enriquecidos, ler metadados em runtime, combinar Annotated com dataclasses, APIs, validação, unidades, NewType e Protocol, além de evitar dependência excessiva de convenções específicas de frameworks.
O problema dos metadados fora do tipo
def criar_usuario(nome: str, idade: int) -> None:
...A assinatura informa que idade é um inteiro, mas não diz que deve estar entre 0 e 130. Essa regra pode aparecer na documentação, em um schema separado ou em código de validação distante.
Annotated permite manter a informação junto da anotação:
from typing import Annotated
Idade = Annotated[int, "0..130"]
def criar_usuario(nome: str, idade: Idade) -> None:
...Para um analisador que ignora metadados, Idade continua sendo int. Uma ferramenta especializada pode interpretar a string e aplicar uma regra.
Sintaxe básica
Annotated[TipoBase, metadado1, metadado2, ...]O primeiro argumento é o tipo real. Os argumentos seguintes podem ser strings, objetos, enums, dataclasses ou qualquer valor Python adequado à ferramenta consumidora.
from dataclasses import dataclass
from typing import Annotated
@dataclass(frozen=True)
class Intervalo:
minimo: int
maximo: int
Percentual = Annotated[int, Intervalo(0, 100)]Usar objetos estruturados costuma ser mais seguro do que strings livres, pois reduz erros de digitação e facilita inspeção.
Annotated não valida sozinho
valor: Percentual = 500O Python não executa automaticamente o objeto Intervalo. A anotação apenas transporta metadados. Um framework, decorador ou função de validação precisa lê-los e aplicar a regra.
Esse ponto é essencial: Annotated não substitui código de runtime nem transforma um int em uma classe validada.
Como verificadores tratam Annotated
Ferramentas de tipagem que não conhecem os metadados devem tratar Annotated[int, ...] como int. Isso preserva compatibilidade e permite que diferentes bibliotecas usem metadados próprios sem quebrar o sistema de tipos central.
def dobro(valor: int) -> int:
return valor * 2
percentual: Percentual = 20
resultado = dobro(percentual)O valor pode ser usado onde int é aceito.
Lendo metadados com get_type_hints
from typing import get_type_hints
def configurar(timeout: Annotated[int, Intervalo(1, 60)]) -> None:
...
hints = get_type_hints(configurar, include_extras=True)
print(hints["timeout"])O parâmetro include_extras=True preserva Annotated e outros detalhes. Sem ele, a função normalmente retorna apenas o tipo base.
Inspecionando origem e argumentos
from typing import get_args, get_origin, Annotated
anotacao = get_type_hints(
configurar,
include_extras=True,
)["timeout"]
print(get_origin(anotacao))
print(get_args(anotacao))get_args() devolve o tipo base seguido dos metadados. Não dependa de atributos internos privados do módulo typing.
Um validador simples
from inspect import signature
from typing import get_args, get_origin, get_type_hints
def validar_chamada(funcao, argumentos: dict[str, object]) -> None:
hints = get_type_hints(funcao, include_extras=True)
for nome, valor in argumentos.items():
anotacao = hints.get(nome)
if get_origin(anotacao) is Annotated:
tipo_base, *metadados = get_args(anotacao)
if not isinstance(valor, tipo_base):
raise TypeError(f"{nome} deve ser {tipo_base}")
for metadado in metadados:
if isinstance(metadado, Intervalo):
if not metadado.minimo <= valor <= metadado.maximo:
raise ValueError(f"{nome} fora do intervalo")Esse exemplo é educacional. Validadores reais precisam lidar com uniões, genéricos, forward references, subclasses, bool versus int e mensagens detalhadas.
Metadados compostos
@dataclass(frozen=True)
class Descricao:
texto: str
@dataclass(frozen=True)
class Unidade:
nome: str
Temperatura = Annotated[
float,
Unidade("celsius"),
Intervalo(-273, 1000),
Descricao("Temperatura medida pelo sensor"),
]Várias ferramentas podem consumir partes diferentes. Um gerador de documentação lê Descricao, um validador usa Intervalo e uma camada de apresentação interpreta Unidade.
Ordem dos metadados
A ordem é preservada e pode importar para a biblioteca consumidora. Evite escrever aplicações que dependam de uma ordem implícita sem documentação. Prefira buscar metadados por tipo quando a sequência não representa um pipeline.
Annotated aninhado
Base = Annotated[int, "base"]
Especial = Annotated[Base, "especial"]Ferramentas podem achatar metadados de Annotated aninhados. A ordem resultante segue regras definidas pelo typing. Para APIs públicas, prefira uma única anotação claramente composta e teste a inspeção nas versões suportadas.
Aliases reutilizáveis
IdPositivo = Annotated[int, Intervalo(1, 2_147_483_647)]
NomeCurto = Annotated[str, "1..80 caracteres"]Aliases reduzem repetição e centralizam convenções. Porém, alterar um alias compartilhado afeta muitas APIs; trate-o como parte do contrato público.
Annotated e NewType
NewType cria distinção estática; Annotated adiciona metadados. Eles resolvem problemas diferentes e podem ser combinados.
from typing import NewType
UsuarioId = NewType("UsuarioId", int)
UsuarioIdValidado = Annotated[UsuarioId, Intervalo(1, 2_147_483_647)]O analisador mantém a identidade de UsuarioId, e uma ferramenta de runtime pode verificar o intervalo. Veja também o guia de NewType no Python.
Annotated e Literal
from typing import Literal
Formato = Annotated[
Literal["json", "csv"],
Descricao("Formato de exportação"),
]Literal restringe valores estaticamente; Annotated adiciona documentação ou comportamento para ferramentas.
Annotated em dataclasses
from dataclasses import dataclass
@dataclass
class Produto:
nome: Annotated[str, Descricao("Nome público")]
preco: Annotated[float, Intervalo(0, 1_000_000)]A dataclass não aplica os metadados por padrão. Uma biblioteca pode inspecionar __annotations__ ou get_type_hints() para gerar validação e schemas.
Annotated em APIs web
Frameworks podem usar metadados para indicar origem de parâmetros, limites, descrições e exemplos. O padrão permite que a assinatura continue sendo compreensível por verificadores enquanto o framework recebe informações extras.
# Exemplo conceitual; a classe Query depende do framework
Limite = Annotated[int, Query(minimo=1, maximo=100)]Ao usar uma biblioteca específica, siga a documentação oficial dela. Não suponha que todos os frameworks interpretem os mesmos objetos.
Metadados como protocolo de biblioteca
Os objetos colocados em Annotated formam uma linguagem entre seu código e a ferramenta consumidora. Defina claramente quais objetos são aceitos, se metadados repetidos são permitidos, como conflitos são resolvidos e em que ordem são aplicados.
Unidades de medida
Metros = Annotated[float, Unidade("m")]
Segundos = Annotated[float, Unidade("s")]
def velocidade(
distancia: Metros,
tempo: Segundos,
) -> Annotated[float, Unidade("m/s")]:
return distancia / tempoPara o analisador, todos ainda são floats e podem ser misturados. Se impedir essa mistura for importante, use NewType ou classes de valor. Annotated sozinho serve como metadado, não como distinção nominal.
Segurança e dados não confiáveis
Não confie em Annotated como barreira de segurança. Um chamador pode ignorar a anotação, e o Python não aplica a regra automaticamente. Valide permissões, limites e formatos em runtime nas fronteiras adequadas.
Forward references
get_type_hints() pode resolver referências futuras usando namespaces do módulo. Em sistemas de plugins, importações condicionais ou classes locais, forneça globalns e localns quando necessário. Inspeção de anotações pode executar avaliação de referências; não trate metadados de código não confiável como dados inertes.
Preservação em decoradores
Decoradores podem substituir ou copiar anotações. Use functools.wraps e evite sobrescrever __annotations__ sem necessidade. Para wrappers tipados, ParamSpec preserva parâmetros estaticamente, enquanto Annotated precisa permanecer disponível para a ferramenta de runtime.
Serialização de metadados
Objetos arbitrários dentro de Annotated não são automaticamente serializáveis. Se os metadados precisam virar JSON, documentação remota ou cache, prefira dataclasses simples, enums e valores imutáveis com conversão explícita.
Compatibilidade de versões
Annotated está disponível em versões modernas do módulo typing. Para versões anteriores, use typing_extensions.Annotated. Verifique também se a biblioteca consumidora suporta include_extras=True e a versão de Python adotada.
Annotated versus comentários e docstrings
Docstrings são melhores para explicações longas e comportamento geral. Annotated é útil para metadados estruturados ligados a um parâmetro ou retorno. Não coloque grandes textos ou regras ambíguas quando uma classe de metadado clara resolveria melhor.
Annotated versus classe de valor
Uma classe de valor pode validar no construtor, oferecer métodos e existir em runtime. Annotated mantém o valor original e depende de um consumidor externo. Use classes para invariantes fortes e comportamento; use Annotated para integração e descrição.
Erros comuns
- Esperar validação automática: Annotated apenas carrega metadados.
- Usar strings sem convenção: erros de digitação ficam difíceis de detectar.
- Esquecer include_extras=True: os metadados desaparecem na inspeção.
- Confiar em Annotated para segurança: chamadas normais podem ignorá-lo.
- Usar para distinção nominal: dois Annotated[int, ...] continuam sendo ints para o checker.
- Acoplar domínio a um framework: aliases centrais podem ficar difíceis de reutilizar.
Exemplo completo: configuração validada
from dataclasses import dataclass
from typing import Annotated, get_args, get_origin, get_type_hints
@dataclass(frozen=True)
class Minimo:
valor: float
@dataclass(frozen=True)
class Maximo:
valor: float
Porta = Annotated[int, Minimo(1), Maximo(65535)]
Timeout = Annotated[float, Minimo(0.1), Maximo(120.0)]
@dataclass
class Configuracao:
porta: Porta
timeout: Timeout
def validar_dataclass(objeto: object) -> None:
hints = get_type_hints(type(objeto), include_extras=True)
for nome, anotacao in hints.items():
valor = getattr(objeto, nome)
if get_origin(anotacao) is not Annotated:
continue
tipo_base, *metadados = get_args(anotacao)
if not isinstance(valor, tipo_base):
raise TypeError(f"{nome}: tipo inválido")
for item in metadados:
if isinstance(item, Minimo) and valor < item.valor:
raise ValueError(f"{nome}: abaixo do mínimo")
if isinstance(item, Maximo) and valor > item.valor:
raise ValueError(f"{nome}: acima do máximo")
config = Configuracao(porta=8000, timeout=5.0)
validar_dataclass(config)O exemplo mantém tipos simples, centraliza metadados e aplica uma política explícita. Bibliotecas maduras devem lidar com herança, campos opcionais, coleções e relatórios de múltiplos erros.
Boas práticas
Use objetos imutáveis e nomeados como metadados. Documente quem os consome. Mantenha regras críticas também em código de runtime. Teste a inspeção com todas as versões suportadas. Evite espalhar objetos de frameworks por todo o domínio quando aliases em uma camada de interface forem suficientes.
Conclusão
typing.Annotated permite transportar metadados junto de tipos sem alterar o comportamento básico da tipagem estática. Ele é útil para validação, schemas, documentação, unidades, serialização e integração com frameworks.
A documentação oficial de Annotated no Python define sua semântica. Trate os metadados como um protocolo explícito entre o código e a ferramenta consumidora, e escolha NewType ou classes de valor quando precisar de distinção ou invariantes reais.







