Pydantic v2: o guia para validar dados em Python
Se você já escreveu Python por mais de alguns meses, provavelmente já passou por isso: uma API recebe um JSON, um script lê um .env, um consumidor de fila processa uma mensagem — e, em algum momento, alguém esquece de checar se o campo idade realmente veio como número, ou se o email não está vazio. O programa não quebra na hora. Ele quebra três camadas depois, num lugar completamente diferente do código, e você passa a tarde inteira com um debugger tentando entender como um None foi parar onde deveria haver uma string.
Esse é o problema que o Pydantic resolve. E é sobre ele — na sua versão mais recente, a 2 — que vamos falar aqui, com calma, do conceito básico até os recursos que só devs mais experientes costumam explorar.
O problema da validação de dados em Python
Python é uma linguagem de tipagem dinâmica. Isso é ótimo para prototipar rápido, mas tem um preço: nada impede que uma função pensada para receber um int receba uma string, um None ou um objeto qualquer. O interpretador só vai reclamar quando aquele valor errado for efetivamente usado — e aí o erro aparece longe da causa raiz.
A PEP 484 tentou amenizar isso introduzindo type hints. Você pode escrever:
def criar_usuario(nome: str, idade: int) -> None:
...
Só que aqui vai um detalhe importante: type hints, por padrão, não validam nada em tempo de execução. Eles são só uma anotação para o seu editor, para ferramentas como mypy, e para quem lê o código depois. Se você chamar criar_usuario("John Smith", "vinte e cinco"), o Python vai deixar passar numa boa — o erro só vai aparecer (se aparecer) quando idade for usado como número mais adiante.
É exatamente essa lacuna — entre "eu documentei o tipo esperado" e "eu garanto que o tipo recebido está correto" — que o Pydantic preenche.
O que é o Pydantic?
O Pydantic é uma biblioteca de validação e parsing de dados que usa as próprias type hints do Python como fonte de verdade. Em vez de você escrever if not isinstance(idade, int): raise ValueError(...) para cada campo, você declara um modelo:
from pydantic import BaseModel
class Usuario(BaseModel):
nome: str
idade: int
E o Pydantic cuida do resto: converte tipos compatíveis, rejeita o que não faz sentido, e devolve um erro claro e estruturado quando algo está errado. A ideia central é simples de enunciar, mas poderosa na prática: o type hint deixa de ser apenas documentação e passa a ser um contrato que é verificado em tempo real.
Um pouco de contexto histórico
O Pydantic começou como uma biblioteca relativamente nichada, mas ganhou tração enorme a partir do momento em que o FastAPI — hoje um dos frameworks web mais usados do ecossistema Python — o adotou como peça central para validar requisições, respostas e gerar documentação OpenAPI automaticamente. Quem aprendia FastAPI acabava aprendendo Pydantic junto, e isso acelerou muito sua adoção.
A grande virada, porém, foi a versão 2. O time reescreveu o núcleo de validação em Rust — o chamado pydantic-core — e manteve a API Python por cima. O resultado é uma biblioteca que parece a mesma para quem usa, mas que por baixo dos panos é ordens de magnitude mais rápida que a v1, que era escrita inteiramente em Python puro.
Como o Pydantic funciona por dentro (na prática)
Vale entender o fluxo, porque isso ajuda a debugar quando algo não sai como esperado. Quando você chama Usuario(nome="John Smith", idade="25"), acontece o seguinte:
- Parsing: o Pydantic recebe os dados brutos (um dicionário, um JSON, kwargs).
- Coerção de tipos: se o valor recebido for compatível, ele é convertido — a string
"25"vira o inteiro25. - Validação: cada campo passa pelas regras declaradas (tipo, restrições do
Field, validadores customizados). - Construção do objeto: se tudo passar, uma instância do modelo é criada, imutável em sua estrutura de tipos.
- Serialização (quando necessário): o objeto pode ser convertido de volta para dicionário ou JSON, com regras próprias de formatação.
Esse é o pipeline que sustenta praticamente tudo que vem a seguir.
Instalando
A instalação é direta:
pip install pydantic
Alguns validadores especiais dependem de pacotes extras. Para validar e-mails, por exemplo, você precisa de:
pip install "pydantic[email]"
Primeiro exemplo, passo a passo
from pydantic import BaseModel
class Usuario(BaseModel):
nome: str
idade: int
ativo: bool = True
usuario = Usuario(nome="John Smith", idade="30")
print(usuario)
# nome='John Smith' idade=30 ativo=True
Repare em dois detalhes:
- Passamos
idade="30"como string, e o Pydantic converteu automaticamente paraint. Essa coerção é intencional — ela reflete cenários reais, como dados vindos de formulários HTML ou query strings, que chegam sempre como texto. - O campo
ativotem um valor padrão (True), então não precisa ser informado.
Se enviarmos algo que não pode ser convertido, o erro aparece de forma clara:
Usuario(nome="John Smith", idade="trinta")
# pydantic.ValidationError: 1 validation error for Usuario
# idade
# Input should be a valid integer, unable to parse string as an integer
Tipos suportados
O Pydantic reconhece nativamente praticamente todo o vocabulário de tipos do Python, incluindo os módulos padrão da biblioteca:
- Tipos primitivos:
str,int,float,bool,Decimal - Data e hora:
datetime,date,time UUID,Path- Rede:
IPv4Address,IPv6Address EnumeLiteral- Estruturas:
list,set,tuple,dict - Combinações com
OptionaleUnion
Isso significa que você raramente precisa "reinventar" um validador para algo comum — o tipo certo, sozinho, já carrega boa parte da validação.
Exemplos práticos de validação do dia a dia
Vamos ver como isso se traduz em casos reais que aparecem o tempo todo em sistemas brasileiros.
E-mail e URL, usando os tipos especiais do Pydantic:
from pydantic import BaseModel, EmailStr, HttpUrl
class Contato(BaseModel):
email: EmailStr
site: HttpUrl
CPF, com validação personalizada (o Pydantic não valida CPF nativamente, mas oferece o gancho certo para isso via field_validator):
from pydantic import BaseModel, field_validator
class Cliente(BaseModel):
cpf: str
@field_validator("cpf")
@classmethod
def validar_cpf(cls, valor: str) -> str:
digitos = "".join(filter(str.isdigit, valor))
if len(digitos) != 11:
raise ValueError("CPF deve conter 11 dígitos")
return digitos
Valor monetário com Decimal — e aqui vale um destaque de boas práticas: nunca use float para dinheiro, pois ele introduz erros de arredondamento por natureza. Decimal é a escolha correta:
from decimal import Decimal
from pydantic import BaseModel
class Pedido(BaseModel):
valor_total: Decimal
Data de nascimento com idade mínima, combinando tipo nativo e validador:
from datetime import date
from pydantic import BaseModel, field_validator
class Cadastro(BaseModel):
nascimento: date
@field_validator("nascimento")
@classmethod
def maior_de_idade(cls, valor: date) -> date:
idade = (date.today() - valor).days // 365
if idade < 18:
raise ValueError("cadastro permitido apenas para maiores de 18 anos")
return valor
O padrão que se repete em todos esses exemplos: o tipo cuida do formato básico, e o field_validator cuida da regra de negócio específica.
Refinando campos com Field
Quando o tipo sozinho não é suficiente, o Field permite adicionar restrições, metadados e valores padrão dinâmicos:
from pydantic import BaseModel, Field
class Produto(BaseModel):
nome: str = Field(..., min_length=3, max_length=100)
preco: float = Field(..., gt=0, description="Preço em reais, deve ser positivo")
quantidade: int = Field(default=0, ge=0)
codigo: str = Field(..., pattern=r"^[A-Z]{3}-\d{4}$")
Aqui gt (greater than), ge (greater or equal), min_length, max_length e pattern (regex) description substituem dezenas de linhas de if que, de outra forma, você teria que escrever manualmente — e, pior, manter sincronizadas em vários lugares do código.
Validadores personalizados: field_validator e model_validator
Nem toda regra é sobre um único campo isolado. Às vezes a validação depende da relação entre dois ou mais campos — por exemplo, garantir que uma "data de fim" seja posterior à "data de início". Para isso existe o model_validator:
from pydantic import BaseModel, model_validator
from datetime import date
class Reserva(BaseModel):
inicio: date
fim: date
@model_validator(mode="after")
def checar_periodo(self) -> "Reserva":
if self.fim <= self.inicio:
raise ValueError("a data de fim deve ser posterior à data de início")
return self
O parâmetro mode merece atenção: mode="before" roda antes da coerção de tipos (útil para normalizar dados brutos), e mode="after" roda depois, já com os campos convertidos e prontos — como no exemplo acima.
Tratamento de erros
Quando a validação falha, o Pydantic levanta uma ValidationError — mas, diferente de uma exceção genérica, ela carrega informação estruturada, fácil de transformar em resposta de API:
from pydantic import ValidationError
try:
Usuario(nome="John Smith", idade="trinta")
except ValidationError as erro:
print(erro.errors())
# [{'type': 'int_parsing', 'loc': ('idade',), 'msg': 'Input should be a valid integer', ...}]
print(erro.json())
O método errors() devolve uma lista de dicionários com o campo (loc), o tipo do erro e a mensagem — perfeito para devolver, por exemplo, um 400 Bad Request detalhado em uma API REST.
Para casos onde a mensagem padrão não é boa o suficiente, dá para criar erros personalizados com PydanticCustomError, definindo código e mensagem parametrizada — algo bastante útil quando o mesmo erro precisa ser exibido de forma diferente em português e em outro idioma, por exemplo.
Serialização
O caminho de volta — de objeto Python para dicionário ou JSON — usa model_dump() e model_dump_json():
usuario = Usuario(nome="John Smith", idade=30)
usuario.model_dump()
# {'nome': 'John Smith', 'idade': 30, 'ativo': True}
usuario.model_dump_json()
# '{"nome":"John Smith","idade":30,"ativo":true}'
usuario.model_dump(exclude={"ativo"})
usuario.model_dump(by_alias=True)
Os parâmetros include, exclude, by_alias e exclude_none dão controle fino sobre o que sai — algo essencial quando, por exemplo, você não quer expor um campo de senha na resposta de uma API, mesmo que ele exista no modelo interno.
O que há de novo na v2
Para quem já usava a v1, alguns nomes e conceitos mudaram. Vale destacar os principais:
model_validate: substitui o antigoparse_obj, usado para criar um modelo a partir de um dicionário já existente.TypeAdapter: permite validar e serializar tipos que não são umBaseModel— umalist[int], por exemplo — sem precisar criar uma classe só para isso. É um dos recursos mais úteis e menos divulgados da v2.computed_field: cria campos derivados de outros, calculados automaticamente na serialização.field_serializer: customiza como um campo específico é convertido na hora da serialização.RootModel: para modelos cujo "corpo" é o próprio valor, sem um dicionário de campos nomeados por baixo (por exemplo, uma lista pura).ConfigDict: substitui a antiga classe internaConfig, centralizando configurações do modelo.- Strict Mode: desliga a coerção automática de tipos quando você quer validação estrita — útil em cenários onde receber
"25"no lugar de25deveria ser um erro, não uma conversão silenciosa.
Performance: por que a v2 é tão mais rápida
A resposta curta é pydantic-core. A v1 fazia toda a validação em Python puro; a v2 delega essa parte para uma biblioteca escrita em Rust, com a API Python funcionando como uma camada fina por cima. Os benchmarks oficiais do projeto mostram ganhos expressivos — em muitos cenários, a v2 valida dados de 5x a 50x mais rápido que a v1, dependendo da complexidade do modelo.
Comparado a alternativas do ecossistema — dataclasses puro (que não valida nada em tempo de execução), attrs, Marshmallow ou msgspec — vale a régua certa: msgspec costuma vencer em performance bruta por ser mais minimalista, mas o Pydantic entrega um equilíbrio raro entre velocidade, ergonomia da API e um ecossistema de integrações (FastAPI, SQLModel, etc.) que dificilmente qualquer alternativa cobre por completo.
Na prática, para a maioria dos sistemas — APIs, ETLs, microsserviços — a performance do Pydantic v2 deixou de ser um fator limitante. Ela só importa de verdade em cenários de altíssimo volume, tipo processamento de milhões de mensagens por segundo.
Onde o Pydantic realmente brilha
- APIs REST, especialmente com FastAPI, onde ele valida request, response e ainda gera a documentação OpenAPI automaticamente.
- Configuração de aplicações, lendo e validando variáveis de ambiente (
.env) através do pacote complementarpydantic-settings. - Pipelines de ETL e Data Science, garantindo que dados vindos de fontes externas (CSV, JSON, APIs de terceiros) estejam no formato esperado antes de seguir para o processamento.
- Microsserviços com filas (RabbitMQ, Kafka), validando o payload de cada mensagem antes de processá-la — evitando que uma mensagem malformada derrube um consumidor inteiro.
- Integração entre sistemas, especialmente quando cada lado usa uma convenção de nomenclatura diferente (camelCase de um lado, snake_case do outro) — resolvido de forma elegante com
aliasnoField.
Pontos positivos
- Código de validação drasticamente mais enxuto e legível.
- Documentação oficial excelente, com exemplos claros.
- Aproveita type hints nativos — nada de sintaxe proprietária esquisita.
- Performance sólida graças ao
pydantic-core. - Ecossistema robusto: FastAPI, SQLModel, pydantic-settings, e mais.
- Facilita manutenção: a regra de validação vive em um único lugar.
Pontos de atenção
- Curva de aprendizado real quando você começa a usar recursos mais avançados (
Annotated,TypeAdapter, validadores condicionais). - Migrar um projeto grande da v1 para a v2 dá trabalho — vários nomes de métodos e comportamentos mudaram.
- Para scripts pequenos e descartáveis, pode ser overhead desnecessário.
- Adiciona uma dependência externa ao projeto.
- A coerção automática de tipos pode surpreender quem espera validação estrita por padrão — mas isso tem solução direta com o Strict Mode.
Boas práticas que valem a pena adotar
- Prefira sempre
Decimalpara valores monetários, nuncafloat. - Use Strict Mode quando o contexto exigir validação rígida, sem conversões implícitas.
- Evite colocar regra de negócio complexa dentro do modelo — o Pydantic deve validar formato e integridade, não orquestrar lógica de domínio inteira.
- Separe validação de dados (Pydantic) de validação de regras de domínio (sua camada de serviço).
- Use
Annotatedpara reaproveitar restrições entre vários campos ou modelos, seguindo o espírito da PEP 593:
from typing import Annotated
from pydantic import BaseModel, Field
CPF = Annotated[str, Field(min_length=11, max_length=11)]
class Cliente(BaseModel):
cpf: CPF
class Fornecedor(BaseModel):
cpf: CPF
Conclusão
O Pydantic vale a pena sempre que dados externos — de uma API, de um arquivo, de uma fila, de uma variável de ambiente — precisam ser transformados em algo confiável antes de circular pelo resto do sistema. Ele transforma validação, que normalmente é um código chato e repetitivo, em algo declarativo, legível e fácil de manter.
Talvez seja exagero para um script de uso único, rodado uma vez e descartado. Mas em qualquer aplicação que vá crescer, receber contribuições de outras pessoas, ou lidar com dados de fontes que você não controla, o investimento em aprender Pydantic v2 se paga rápido — em bugs que simplesmente deixam de existir, porque nunca chegaram a acontecer.