Annotated no Python: tipos com metadados

Publicado em: 29/08/2026
Tempo de leitura: 7 minutos
A person typing on a laptop with a Python programming book visible, capturing technology and learning.

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 = 500

O 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 / tempo

Para 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.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Conteúdo do artigo

    Artigos relacionados

    Laptop displaying code editor on a desk with a coffee mug beside it, suggesting a workspace or home office setting.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    NewType no Python: IDs sem misturar

    Aprenda NewType no Python para separar IDs, códigos e valores primitivos, validar fronteiras e evitar misturas sem criar classes pesadas.

    Ler mais

    Tempo de leitura: 6 minutos
    29/08/2026
    Close-up view of a computer screen displaying code in a software development environment.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    Never no Python: marque código inalcançável

    Aprenda typing.Never no Python para funções que não retornam, código inalcançável e verificação exaustiva com assert_never.

    Ler mais

    Tempo de leitura: 6 minutos
    29/08/2026
    Macro shot capturing detailed patterns of a python in its natural surroundings.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    Concatenate no Python: altere parâmetros

    Aprenda Concatenate no Python para adicionar ou ocultar parâmetros em decoradores tipados com ParamSpec, contexto e dependências.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    A close-up view of a person's hand signing a business contract on a desk with a pen.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    ParamSpec no Python: preserve assinaturas

    Aprenda ParamSpec no Python para preservar assinaturas em decoradores, callbacks, wrappers async e funções de ordem superior.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    A detailed image of a reticulated python showcasing its patterned scales and intricate skin texture.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    TypeIs no Python: refine os dois ramos

    Aprenda TypeIs no Python para refinar tipos nos ramos verdadeiro e falso, comparar com TypeGuard e criar predicados seguros.

    Ler mais

    Tempo de leitura: 6 minutos
    28/08/2026
    Close-up of hands typing on a laptop keyboard, Python book in sight, coding in progress.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    TypeGuard no Python: refine tipos com segurança

    Aprenda TypeGuard no Python para refinar tipos, validar coleções, TypedDict e Protocol com segurança estática e checagem real.

    Ler mais

    Tempo de leitura: 6 minutos
    28/08/2026