Annotated do Python: O Segredo por Trás das Grandes Bibliotecas
Vamos entender um carinha que você provavelmente já viu por aí, mas talvez nunca tenha parado para criar no seu próprio código: o Annotated do Python.
Se você já trabalhou com bibliotecas como Pydantic, FastAPI, LangChain ou LangGraph, com certeza já se deparou com o Annotated. Ele aparece ali, quietinho nas anotações de tipo, e a gente usa sem nem pensar muito. Mas você já se perguntou o que ele realmente faz e por que é tão poderoso?
Eu garanto que, depois desta leitura, você vai querer colocar o Annotated nos seus projetos. A ideia aqui é irmos de 0 a 100 bem rápido e explodir sua cabeça com as possibilidades.
Em Vídeo
Se preferir, você pode assistir a este conteúdo no meu canal do Youtube.
O que é o Annotated?
De forma simples, o Annotated é um tipo especial que nos permite adicionar metadados a outros tipos. Pense nele como uma forma de "anotar" seus type hints com informações extras que podem ser usadas em runtime.
A sintaxe básica é:
Annotated[Tipo, Metadado1, Metadado2, ...]
O primeiro argumento é o tipo real que os type-checkers (como MyPy ou Pyright) vão considerar. Para eles,
Annotated[int, "alguma coisa"]é simplesmente umint.Do segundo argumento em diante, a coisa fica interessante. Você pode passar praticamente qualquer coisa: strings, funções, classes, objetos... o céu é o limite. Esses são os seus metadados.
Vamos ver um exemplo bobo só para começar:
from typing import Annotated
# Para o type-checker, isso é um `int`.
# Para nós, em runtime, é um `int` com metadados.
Inteiro = Annotated[int, "Um número inteiro"] # int para o type checkerNesse primeiro momento, não parece grande coisa, né? Mas é aqui que a mágica começa.
O Poder do Runtime: Acessando os Metadados
A verdadeira força do Annotated é que esses metadados não se perdem depois da checagem de tipo. Nós podemos acessá-los em runtime para criar lógicas incríveis. É por isso que as bibliotecas modernas abusam dele!
Para inspecionar essas anotações, usamos algumas funções do módulo typing:
get_type_hints(obj, include_extras=True): Pega as anotações de tipo de um objeto (função, classe, etc.). Oinclude_extras=Trueé crucial para que os metadados doAnnotatedsejam incluídos.get_args(tipo): Retorna os argumentos de um tipo. Para umAnnotated, ele devolve uma tupla com o tipo original e todos os metadados.get_origin(tipo): Retorna o "container" do tipo. ParaAnnotated[...], ele retornaAnnotated.
Mão na Massa: Criando um Validador com Annotated
Vamos criar algo realmente útil: um decorator que valida os argumentos de uma função em runtime, inspirado no que o Pydantic faz.
Nosso objetivo: decorar uma função e fazer com que ela lance um erro se os argumentos não atenderem a certas condições (como tamanho máximo de uma string ou um range numérico).
O Contrato do Validador
Primeiro, vamos definir um "contrato" para nossos validadores usando uma classe base abstrata (ABC). Isso garante que qualquer validador que criarmos terá um método validate.
class ValidationError(Exception):
"""Uma exceção com nome mais semântico"""
class Validator[T](ABC):
"""Contrato que os validadores prometem cumprir"""
@abstractmethod
def validate(self, value: T) -> T: ...Implementando Validadores
Agora, vamos criar duas classes que seguem esse contrato.
class IntRange(Validator[int]):
"""Valida um range de números"""
def __init__(self, min_: int, max_: int) -> None:
self.min = min_
self.max = max_
def validate(self, value: int) -> int:
if value < self.min or value > self.max:
msg = f"{value} is out of range ({self.min}, {self.max})"
raise ValidationError(msg)
return value
class StrMaxLength(Validator[str]):
"""Valida uma string com tamanho máximo"""
def __init__(self, max_: int) -> None:
self.max = max_
@validate_annotated
def validate(self, value: str) -> str:
if len(value) > self.max:
msg = f"{value!r} has more than {self.max} characters"
raise ValidationError(msg)
return valueO Decorator Inteligente
Aqui está o coração da nossa lógica. Este decorator vai:
Inspecionar os
type hintsda função que ele decora.Procurar por argumentos anotados com nossos
Validators.Executar a validação antes de chamar a função original.
def validate_annotated[**P, R](func: Callable[P, R]) -> Callable[P, R]:
"""Decorator que valida o que for anotado com Annotated e Validator"""
# Obtemos as annotations da função
hints = get_type_hints(func, include_extras=True)
# Isso vai nos ajudar a inspecionar a função
signature = inspect.signature(func)
@wraps(func) # Boa prática 👍
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
# Agora podemos pegar os argumentos e seus valores
bound_arguments = signature.bind(*args, **kwargs)
bound_arguments.apply_defaults()
# Se não encontrarmos nenhuma annotation, retornamos
if not hints:
return func(*args, **kwargs)
# Loop nos nomes dos argumentos da função
for arg_name in bound_arguments.arguments:
# Se o nome que eu recebi aqui não está anotado, próximo...
if arg_name not in hints:
continue
# Pego o valor do argumento
value = bound_arguments.arguments[arg_name]
# Pego os dados do argumento
metadata = get_args(hints[arg_name])
# Só vou conferir algo que tenha mais de um valor
# o segundo valor pode ser um ou mais validadores
if len(metadata) <= 1:
continue
# Pegamos o tipo e os possíveis validadores
type_, *validators = metadata
# Vamos passar em todos checando se são mesmo validadores
for validator in validators:
if not isinstance(validator, Validator):
continue
# Se são, quero garantir que o tipo também está correto
if not isinstance(value, type_):
msg = (
f"Argument {arg_name!r} should be of type {type_!r} "
f"in {func.__name__!r}. Got {type(value)!r}."
)
raise TypeError(msg)
# E agora é só validar
# Obs.: Já chequei o tipo, pyright não reconheceu
validator.validate(value) # pyright: ignore[reportUnknownMemberType]
# Executamos a função original com os argumentos bonitinhos
return func(*args, **kwargs)
# Te dou uma nova função
return wrapperUsando a Mágica!
Agora, podemos usar nosso decorator e o Annotated de forma limpa e declarativa.
@validate_annotated
def set_height(height: Annotated[int, IntRange(1, 100)]) -> int:
"""Set the widget height"""
p(f"Height is set to {height}")
return height
@validate_annotated
def set_app_name(
name: Annotated[str, StrMaxLength(10)],
) -> str:
"""Change app name"""
p(f"App name is now {name!r}")
return name
if __name__ == "__main__":
new_app_name = set_app_name(name="My App")
new_height = set_height(height=1)Olha que massa! Se você brincar com os valores de `name` e `height` nos testes acima, em algum momento vai ver erros de validação na tela.
A lógica de validação está completamente separada da lógica de negócio da função. Nós apenas declaramos as regras usando Annotated, e o decorator faz todo o trabalho sujo por baixo dos panos.
Conclusão
Espero que isso tenha explodido sua cabeça tanto quanto explodiu a minha na primeira vez que entendi o poder do Annotated. Ele é a ponte entre a checagem de tipos estática e o comportamento dinâmico em runtime.
As possibilidades são enormes:
Validação de dados (como vimos).
Documentação automática de APIs.
Injeção de dependências.
Configuração de serializadores.
Então, da próxima vez que você vir um Annotated em uma biblioteca, vai saber que não é só um type hint enfeitado, mas sim uma porta de entrada para um mundo de metaprogramação poderosa e elegante.
Volte a este artigo, brinque com o código e comece a pensar em como você pode usar o Annotated para tornar seus próprios projetos mais robustos e expressivos.
Beijo, me liga!


