Artigo

Validação de Dados com Pydantic: Guia Completo para Python

Validação de Dados com Pydantic: Guia Completo para Python
Tempo de leitura9 minutos

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 dict cru em vez de modelo — perde a validação.
  • Confundir model_dump() (dict) com model_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

  1. Modele a entrada e a saída com tipos claros.
  2. Use Field para restrições e documentação.
  3. Valide regras de formato com field_validator.
  4. Valide regras entre campos com model_validator.
  5. Use pydantic-settings para configuração.
  6. 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.

Anterior e próximo

Leituras recentes

Temas parecidos para continuar navegando no blog.

Conteúdo criado para leitura limpa, boa hierarquia visual e navegação clara.