Pydantic: O que É e Como Funciona | Python Brasil
Guia completo do Pydantic v2: validação de dados em Python com BaseModel, computed fields, settings, serialização e integração com FastAPI.
O que é Pydantic?
O Pydantic é a biblioteca mais popular do Python para validação de dados usando type hints. Ele garante que os dados recebidos estejam no formato correto, fazendo conversões automáticas e levantando erros claros quando algo está errado.
É a biblioteca padrão de validação do FastAPI e é amplamente usada em projetos profissionais para parsing de configurações, validação de entradas de API, integração com bancos de dados e muito mais.
Pydantic v1 vs v2
Em 2023, o Pydantic lançou a versão 2, uma reescrita completa com o núcleo implementado em Rust. As diferenças principais são:
| Aspecto | v1 | v2 |
|---|---|---|
| Performance | Python puro | Núcleo em Rust, até 50x mais rápido |
| Validator | @validator | @field_validator e @model_validator |
| Configuração | classe Config | model_config = ConfigDict(...) |
| Serialização | .dict(), .json() | .model_dump(), .model_dump_json() |
| Schema | .schema() | .model_json_schema() |
A v2 é a versão atual e recomendada. A v1 ainda recebe correções de segurança, mas não receberá novas funcionalidades.
Exemplo Básico
from pydantic import BaseModel, EmailStr, field_validator
from datetime import date
from typing import Optional
class Usuario(BaseModel):
nome: str
email: str
idade: int
ativo: bool = True
nascimento: Optional[date] = None
# Validação e coerção automáticas
user = Usuario(
nome="Ana Silva",
email="ana@email.com",
idade="28", # String "28" convertida para int!
nascimento="1997-05-15" # String convertida para date!
)
print(user)
# nome='Ana Silva' email='ana@email.com' idade=28 ativo=True nascimento=date(1997, 5, 15)
# Erro de validação claro
try:
user_invalido = Usuario(
nome="Bruno",
email="bruno@email.com",
idade="não é número"
)
except Exception as e:
print(e)
# 1 validation error for Usuario
# idade: Input should be a valid integer [type=int_parsing]
Validators Personalizados
from pydantic import BaseModel, field_validator, model_validator
class Produto(BaseModel):
nome: str
preco: float
desconto: float = 0.0
preco_final: float = 0.0
@field_validator("nome")
@classmethod
def nome_nao_vazio(cls, v: str) -> str:
if not v.strip():
raise ValueError("Nome não pode ser vazio")
return v.strip().title()
@field_validator("preco")
@classmethod
def preco_positivo(cls, v: float) -> float:
if v <= 0:
raise ValueError("Preço deve ser positivo")
return round(v, 2)
@field_validator("desconto")
@classmethod
def desconto_valido(cls, v: float) -> float:
if not 0 <= v <= 100:
raise ValueError("Desconto deve estar entre 0 e 100")
return v
@model_validator(mode="after")
def calcular_preco_final(self) -> "Produto":
self.preco_final = self.preco * (1 - self.desconto / 100)
return self
produto = Produto(nome="notebook", preco=2999.999, desconto=10)
print(produto.preco_final) # 2699.997...
model_config
A classe model_config substitui a antiga classe Config da v1:
from pydantic import BaseModel, ConfigDict
class Usuario(BaseModel):
model_config = ConfigDict(
str_strip_whitespace=True, # Remove espaços em strings
str_to_lower=False, # Não converte para minúsculas
frozen=True, # Modelo imutável (como tuple)
populate_by_name=True, # Permite usar nome do campo ou alias
json_schema_extra={ # Documentação extra para OpenAPI
"example": {"nome": "Maria", "email": "maria@email.com"}
}
)
nome: str
email: str
Computed Fields
Campos calculados automaticamente a partir de outros campos:
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
@computed_field
@property
def perimetro(self) -> float:
return 2 * (self.largura + self.altura)
r = Retangulo(largura=5.0, altura=3.0)
print(r.area) # 15.0
print(r.perimetro) # 16.0
print(r.model_dump()) # inclui area e perimetro!
Herança de Modelos
from pydantic import BaseModel
from datetime import datetime
from typing import Optional
class PessoaBase(BaseModel):
nome: str
email: str
class PessoaCreate(PessoaBase):
senha: str # Só necessário na criação
class PessoaResponse(PessoaBase):
id: int
criado_em: datetime
ativo: bool = True
# "senha" não aparece aqui — protege dados sensíveis
class PessoaUpdate(BaseModel):
nome: Optional[str] = None
email: Optional[str] = None
# Todos os campos opcionais para PATCH
# Uso no FastAPI
@app.post("/usuarios", response_model=PessoaResponse)
async def criar_usuario(usuario: PessoaCreate):
# usuario.senha está disponível aqui para hash
# mas PessoaResponse não vai expô-la
...
Modelos Genéricos
from pydantic import BaseModel
from typing import Generic, TypeVar, Optional
T = TypeVar("T")
class RespostaPaginada(BaseModel, Generic[T]):
dados: list[T]
total: int
pagina: int
tamanho: int
paginas: int
@classmethod
def criar(cls, dados: list[T], total: int, pagina: int, tamanho: int):
return cls(
dados=dados,
total=total,
pagina=pagina,
tamanho=tamanho,
paginas=(total + tamanho - 1) // tamanho
)
class Produto(BaseModel):
id: int
nome: str
preco: float
# Uso com tipo específico
resposta: RespostaPaginada[Produto] = RespostaPaginada.criar(
dados=[Produto(id=1, nome="Notebook", preco=4999.0)],
total=100,
pagina=1,
tamanho=20
)
Serialização: model_dump e model_dump_json
from pydantic import BaseModel, Field
from datetime import datetime
class Pedido(BaseModel):
id: int
cliente: str
total: float
criado_em: datetime = Field(default_factory=datetime.utcnow)
pedido = Pedido(id=1, cliente="Maria", total=299.90)
# Para dicionário Python
dados_dict = pedido.model_dump()
# {"id": 1, "cliente": "Maria", "total": 299.9, "criado_em": datetime(...)}
# Excluir campos
sem_data = pedido.model_dump(exclude={"criado_em"})
# Incluir apenas campos específicos
resumo = pedido.model_dump(include={"id", "total"})
# Para string JSON
dados_json = pedido.model_dump_json()
dados_json_indent = pedido.model_dump_json(indent=2)
# Aliases para serialização
class PedidoAPI(BaseModel):
model_config = ConfigDict(populate_by_name=True)
id_pedido: int = Field(alias="orderId")
nome_cliente: str = Field(alias="customerName")
pedido_api = PedidoAPI(orderId=1, customerName="João")
print(pedido_api.model_dump(by_alias=True))
# {"orderId": 1, "customerName": "João"}
Geração de JSON Schema
from pydantic import BaseModel, Field
class Produto(BaseModel):
nome: str = Field(description="Nome do produto", min_length=1, max_length=200)
preco: float = Field(description="Preço em reais", gt=0)
categoria: str = Field(description="Categoria do produto")
# Gera schema JSON compatível com OpenAPI
schema = Produto.model_json_schema()
# {
# "title": "Produto",
# "type": "object",
# "properties": {
# "nome": {"type": "string", "minLength": 1, "maxLength": 200, ...},
# "preco": {"type": "number", "exclusiveMinimum": 0, ...},
# ...
# },
# "required": ["nome", "preco", "categoria"]
# }
Gerenciamento de Settings com pydantic-settings
A biblioteca pydantic-settings usa Pydantic para carregar configurações de variáveis de ambiente e arquivos .env:
# pip install pydantic-settings
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False
)
# Variáveis de ambiente: DATABASE_URL, SECRET_KEY, etc.
database_url: str
secret_key: str
debug: bool = False
max_conexoes: int = 10
nome_app: str = "Minha API"
# Singleton com cache
from functools import lru_cache
@lru_cache
def get_settings() -> Settings:
return Settings()
# Uso no FastAPI
from fastapi import Depends
@app.get("/info")
async def info(settings: Settings = Depends(get_settings)):
return {"app": settings.nome_app, "debug": settings.debug}
O arquivo .env correspondente:
DATABASE_URL=postgresql://user:pass@localhost/mydb
SECRET_KEY=minha-chave-secreta-aqui
DEBUG=false
MAX_CONEXOES=20
Integração com SQLAlchemy
from pydantic import BaseModel, ConfigDict
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class UsuarioORM(Base):
__tablename__ = "usuarios"
id: Mapped[int] = mapped_column(primary_key=True)
nome: Mapped[str]
email: Mapped[str]
# Pydantic com from_attributes para ler de ORM
class UsuarioSchema(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
nome: str
email: str
# Converter ORM para Pydantic
usuario_orm = db.query(UsuarioORM).first()
usuario_pydantic = UsuarioSchema.model_validate(usuario_orm)
Erros Comuns
- Usar
.dict()e.json(): métodos da v1, depreciados na v2; use.model_dump()e.model_dump_json() - Esquecer
@classmethodnos validators:@field_validatorexige@classmethod - Mutação de modelos congelados: se usar
frozen=True, o modelo é imutável; crie uma cópia com.model_copy(update={...}) - Não usar
from_attributes=True: necessário para converter objetos ORM em modelos Pydantic
Boas Práticas
- Separe modelos de entrada (Create/Update) e saída (Response) para proteger dados sensíveis
- Use
Field(description=...)para documentar campos e melhorar o Swagger gerado pelo FastAPI - Aproveite
pydantic-settingspara todas as configurações da aplicação - Prefira modelos genéricos para respostas paginadas e resultados padronizados
Termos Relacionados
- FastAPI - Framework que usa Pydantic nativamente
- Dataclass - Alternativa sem validação automática
- Type Hints - Base do funcionamento do Pydantic
- API REST - Pydantic é essencial para APIs