Dados inválidos são a causa silenciosa da maioria dos bugs em produção. O Pydantic resolve isso na raiz: você declara a forma dos seus dados com tipos Python e a biblioteca garante que nada entre errado. Neste guia completo mostro como usar Pydantic de verdade, do modelo básico até settings, validators customizados e integração com FastAPI.
Vou partir de um caso real — processar pedidos de uma API — e evoluir até um sistema robusto de validação.
O que é Pydantic e por que usar
- Validação automática: você declara os tipos; o Pydantic valida em runtime.
- Conversão de dados: transforma strings em números, datas e enums quando possível.
- Erros claros: mensagens de erro detalhadas por campo.
- Base do FastAPI: é o coração da validação de requisições e respostas.
Instalando
python -m venv .venv && source .venv/bin/activate
pip install pydantic pydantic-settings
O primeiro modelo
Um modelo Pydantic é uma classe que herda de BaseModel e declara campos tipados.
from pydantic import BaseModel
class Cliente(BaseModel):
nome: str
email: str
idade: int
c = Cliente(nome="Ana", email="ana@exemplo.com", idade=30)
print(c)
print(c.model_dump())
Se você passar idade="trinta", o Pydantic levanta um erro claro — antes de qualquer lógica rodar.
Valores padrão e campos opcionais
from typing import Optional
from pydantic import BaseModel
class Produto(BaseModel):
nome: str
preco: float
estoque: int = 0
descricao: Optional[str] = None
Campos com padrão não são obrigatórios. Use Optional para aceitar None explicitamente.
Validação com Field
O Field adiciona restrições e metadados.
from pydantic import BaseModel, Field
class Conta(BaseModel):
saldo: float = Field(ge=0, description="Saldo nao pode ser negativo")
nome: str = Field(min_length=2, max_length=80)
limite: float = Field(default=1000, gt=0)
Validators customizados
Regras de negócio vão em validators. Aqui, garantimos que o email tenha formato válido e o CPF tenha 11 dígitos.
from pydantic import BaseModel, field_validator
class Usuario(BaseModel):
email: str
cpf: str
@field_validator("email")
@classmethod
def email_valido(cls, v: str) -> str:
if "@" not in v:
raise ValueError("email invalido")
return v.lower()
@field_validator("cpf")
@classmethod
def cpf_valido(cls, v: str) -> str:
digitos = "".join(filter(str.isdigit, v))
if len(digitos) != 11:
raise ValueError("CPF deve ter 11 digitos")
return digitos
Modelos aninhados
Dados reais têm hierarquia. O Pydantic valida tudo recursivamente.
class Item(BaseModel):
produto: str
quantidade: int
class Pedido(BaseModel):
cliente: Usuario
itens: list[Item]
total: float
pedido = Pedido(
cliente={"email": "a@b.com", "cpf": "12345678901"},
itens=[{"produto": "teclado", "quantidade": 2}],
total=199.9,
)
print(pedido.model_dump())
Serialização e desserialização
Converter para JSON e de volta é trivial e seguro.
json_str = pedido.model_dump_json(indent=2)
print(json_str)
# de volta
import json
dados = json.loads(json_str)
novo = Pedido.model_validate(dados)
Settings: configuração com validação
O pydantic-settings lê variáveis de ambiente e valida — perfeito para configurações de app.
from pydantic_settings import BaseSettings
class Config(BaseSettings):
database_url: str
api_key: str
debug: bool = False
max_workers: int = 4
model_config = {"env_file": ".env"}
cfg = Config() # le do ambiente/.env e valida
Integração com FastAPI
No FastAPI, o Pydantic valida automaticamente entrada e saída.
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Entrada(BaseModel):
texto: str
class Saida(BaseModel):
resultado: str
@app.post("/processar", response_model=Saida)
def processar(payload: Entrada) -> Saida:
return Saida(resultado=payload.texto.upper())
Se o cliente enviar dado inválido, o FastAPI responde 422 com o detalhe do erro — sem você escrever uma linha de validação.
Erros e exceções
from pydantic import ValidationError
try:
Usuario(email="sem-arroba", cpf="123")
except ValidationError as e:
print(e.errors()) # lista de erros por campo
Erros comuns
- Usar
dictcru em vez de modelo — perde a validação. - Confundir
model_dump()(dict) commodel_dump_json()(string). - Colocar regra de negócio complexa em validator quando deveria ser um serviço.
- Esquecer que validators de campo rodam na ordem de declaração.
- Não versionar modelos de API — quebra clientes.
Checklist de uso
- Modele a entrada e a saída com tipos claros.
- Use
Fieldpara restrições e documentação. - Valide regras de formato com
field_validator. - Valide regras entre campos com
model_validator. - Use
pydantic-settingspara configuração. - Reaproveite os modelos no FastAPI.
Model validators: regras entre campos
Algumas regras dependem de mais de um campo. Nesses casos, use model_validator.
from pydantic import BaseModel, model_validator
class Periodo(BaseModel):
inicio: int
fim: int
@model_validator(mode="after")
def checar_ordem(self):
if self.fim <= self.inicio:
raise ValueError("fim deve ser maior que inicio")
return self
Enums e tipos literais
Restringir valores a um conjunto conhecido evita erros bobos.
from enum import Enum
from pydantic import BaseModel
class Plano(str, Enum):
free = "free"
pro = "pro"
enterprise = "enterprise"
class Assinatura(BaseModel):
plano: Plano
meses: int = 1
Campos computados
Valores derivados de outros campos podem ser expostos automaticamente.
from pydantic import BaseModel, computed_field
class Retangulo(BaseModel):
largura: float
altura: float
@computed_field
@property
def area(self) -> float:
return self.largura * self.altura
Aliases e JSON de terceiros
APIs externas usam nomes diferentes. O alias resolve sem renomear seu campo.
from pydantic import BaseModel, Field
class Usuario(BaseModel):
nome_completo: str = Field(alias="fullName")
u = Usuario.model_validate({"fullName": "Ana Silva"})
print(u.nome_completo)
Configuração do modelo
class Config2(BaseModel):
model_config = {
"str_strip_whitespace": True,
"extra": "forbid", # rejeita campos desconhecidos
"validate_assignment": True,
}
nome: str
Testando modelos
import pytest
from pydantic import ValidationError
def test_cpf_invalido():
with pytest.raises(ValidationError):
Usuario(email="a@b.com", cpf="123")
def test_email_normalizado():
u = Usuario(email="A@B.COM", cpf="12345678901")
assert u.email == "a@b.com"
Performance
O núcleo do Pydantic v2 é escrito em Rust, então a validação é rápida. Para modelos usados em loops intensos, evite revalidar o mesmo objeto repetidamente e prefira model_construct quando os dados já são confiáveis.
Integração com banco de dados
Um padrão comum é validar a entrada com Pydantic e persistir com um ORM.
from pydantic import BaseModel
from sqlmodel import SQLModel, Field
class UsuarioIn(BaseModel):
nome: str
email: str
class UsuarioDB(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
nome: str
email: str
def criar(payload: UsuarioIn) -> UsuarioDB:
return UsuarioDB(**payload.model_dump())
Documentação automática
As descrições do Field viram documentação no Swagger do FastAPI.
class Busca(BaseModel):
termo: str = Field(description="Termo a pesquisar", examples=["pydantic"])
limite: int = Field(default=10, ge=1, le=100, description="Maximo de resultados")
Versionando modelos de API
Mudanças de contrato quebram clientes. Mantenha versões lado a lado.
class ProdutoV1(BaseModel):
nome: str
preco: float
class ProdutoV2(BaseModel):
nome: str
preco: float
moeda: str = "BRL"
Pydantic para validar respostas de LLM
Quando você pede JSON ao modelo, valide com Pydantic antes de usar — modelos alucinam formato.
from pydantic import BaseModel, ValidationError
class Extracao(BaseModel):
nome: str
valor: float
def extrair(json_texto: str):
try:
return Extracao.model_validate_json(json_texto)
except ValidationError:
return None # pede de novo ao modelo
Perguntas frequentes
Pydantic v1 ou v2? Use v2 — o núcleo em Rust é muito mais rápido.
Substitui dataclasses? Para dados que cruzam fronteiras, sim; para estruturas internas simples, dataclass ainda serve.
Funciona com Django? Sim, para validar formulários/JSON de API.
Pydantic em pipelines de dados
Em ETL, validar cada registro evita que dado sujo contamine o destino. Um padrão é validar linha a linha e separar as inválidas para análise.
from pydantic import BaseModel, ValidationError
class Linha(BaseModel):
id: int
valor: float
data: str
def validar_linhas(linhas: list[dict]):
validas, invalidas = [], []
for linha in linhas:
try:
validas.append(Linha.model_validate(linha))
except ValidationError as e:
invalidas.append({"linha": linha, "erro": e.errors()})
return validas, invalidas
Esse padrão é a diferença entre um pipeline que quebra no meio e um que processa o que pode e reporta o resto.
Pydantic com Dataclasses e TypedDict
Nem tudo precisa ser BaseModel. Dataclasses são mais leves para estruturas internas, e o Pydantic consegue validá-las quando necessário.
from dataclasses import dataclass
from pydantic import TypeAdapter
@dataclass
class Ponto:
x: float
y: float
adapter = TypeAdapter(Ponto)
print(adapter.validate_python({"x": 1, "y": 2}))
Migrando de v1 para v2
As mudanças principais: validator → field_validator, parse_obj → model_validate, dict() → model_dump(), json() → model_dump_json(). Migre cedo, o ganho de performance compensa.
Exemplo prático completo: API de pedidos
Vamos juntar tudo em um serviço real, com entrada, validação, regra de negócio e resposta tipada.
from datetime import datetime
from enum import Enum
from pydantic import BaseModel, Field, field_validator, model_validator
class StatusPedido(str, Enum):
novo = "novo"
pago = "pago"
enviado = "enviado"
class ItemPedido(BaseModel):
sku: str = Field(min_length=3)
quantidade: int = Field(gt=0, le=999)
preco_unitario: float = Field(gt=0)
@property
def subtotal(self) -> float:
return self.quantidade * self.preco_unitario
class PedidoEntrada(BaseModel):
cliente_email: str
itens: list[ItemPedido] = Field(min_length=1)
cupom: str | None = None
@field_validator("cliente_email")
@classmethod
def email_ok(cls, v: str) -> str:
if "@" not in v:
raise ValueError("email invalido")
return v.lower()
@model_validator(mode="after")
def limite_itens(self):
if len(self.itens) > 50:
raise ValueError("pedido excede 50 itens")
return self
class PedidoSaida(BaseModel):
id: int
total: float
status: StatusPedido
criado_em: datetime
Note como a validação cobre formato (email), limites (quantidade), cardinalidade (mínimo de itens) e regra de negócio (máximo de itens).
Calculando o total com segurança
def calcular_total(pedido: PedidoEntrada) -> float:
total = sum(item.subtotal for item in pedido.itens)
if pedido.cupom == "DEZ":
total *= 0.9
return round(total, 2)
Validação de tipos complexos
O Pydantic lida com datas, UUIDs, IPs, URLs e muito mais sem código extra.
from uuid import UUID
from datetime import date
from pydantic import BaseModel, HttpUrl, IPvAnyAddress
class Evento(BaseModel):
id: UUID
site: HttpUrl
ip: IPvAnyAddress
quando: date
Resumo
Pydantic transforma validação de dados de uma tarefa chata em algo declarativo e automático. Comece modelando suas entradas, adicione validators para regras de formato e use settings para configuração. O ganho de confiabilidade é imediato — e você escreve menos código de verificação.
