Pydantic na Prática (Parte 1) - Fundamentos e Validação de Dados em Python

Introdução

Se você desenvolve APIs, consome dados externos ou já perdeu tempo debugando um TypeError que só apareceu em produção porque um campo chegou como string em vez de número, este post é para você. Vamos falar sobre Pydantic, uma das bibliotecas mais usadas do ecossistema Python moderno - e que provavelmente já está rodando por baixo dos panos em algum projeto seu, mesmo que você nunca a tenha importado diretamente (o FastAPI, por exemplo, é construído em cima dela).

Esta é a Parte 1 de uma série de dois posts. Aqui vamos cobrir os fundamentos: o que é o Pydantic, como instalar, como criar seus primeiros modelos e como validar dados na prática, com um caso de uso real ao final. Na Parte 2, vamos avançar para tópicos intermediários/avançados: validators customizados, model_config, tipos aninhados, Annotated, Pydantic Settings, performance e mais.

Quem é o autor do Pydantic?

O Pydantic foi criado por Samuel Colvin, engenheiro britânico que começou o projeto em 2017 para resolver um problema recorrente no próprio dia a dia: validar dados de entrada em aplicações Python sem escrever uma montanha de código repetitivo de if/raise. Colvin fundou posteriormente a Pydantic Services Inc., empresa que hoje mantém o projeto e também desenvolve ferramentas relacionadas, como o Pydantic AI (framework para construção de agentes de IA) e o Pydantic Logfire (observabilidade). O projeto é open source, mantido por uma equipe e uma comunidade ativa no GitHub, e é usado por empresas como Amazon, Microsoft, NASA, NVIDIA, Netflix e a própria Anthropic.

Hoje o Pydantic está na sua segunda geração de arquitetura: a partir da versão 2.0, o núcleo de validação foi reescrito em Rust (o pacote pydantic-core), o que trouxe ganhos de performance expressivos em relação à V1 - a documentação oficial cita a validação como uma das mais rápidas entre as bibliotecas Python do gênero. Neste post estamos usando a versão mais recente da série 2.x.

O que é o Pydantic e por que usá-lo?

Pydantic é uma biblioteca de validação e modelagem de dados para Python. Na prática, você declara a “forma” dos seus dados usando type hints nativos do Python (str, int, list[str], datetime, etc.), e o Pydantic cuida de três coisas para você:

  1. Validação - garante que os dados recebidos realmente correspondem aos tipos declarados, e rejeita (com um erro claro) o que não corresponde.
  2. Conversão (coerção) - quando possível e seguro, converte tipos compatíveis automaticamente (por exemplo, a string "25" vira o inteiro 25).
  3. Serialização - transforma os objetos Python de volta em dicionários ou JSON, prontos para trafegar em uma API, ser salvos em banco ou logados.

A grande sacada do Pydantic é que ele usa a sintaxe de tipagem que você já escreve (ou deveria escrever) em Python moderno. Não é uma DSL nova para aprender - é class Usuario(BaseModel) com anotações de tipo, e o resto é automático.

Que problema o Pydantic resolve?

Todo backend que recebe dados de fora (requisição HTTP, mensagem de fila, variável de ambiente, arquivo de configuração, resposta de outra API) enfrenta o mesmo problema: você não pode confiar cegamente no formato dessas informações. Sem uma camada de validação, é comum escrever código assim:

PYTHON
def criar_usuario(dados: dict):
    if "nome" not in dados or not isinstance(dados["nome"], str):
        raise ValueError("nome inválido")
    if "idade" not in dados or not isinstance(dados["idade"], int):
        raise ValueError("idade inválida")
    # ... e assim por diante, para cada campo
Clique para expandir e ver mais

Esse código cresce, fica repetitivo, é fácil esquecer uma verificação e difícil de manter conforme o modelo de dados evolui. O Pydantic resolve isso substituindo essas verificações manuais por uma declaração única e tipada, validada automaticamente toda vez que um objeto é criado. O resultado é menos código boilerplate, mensagens de erro padronizadas e muito mais confiança em qualquer fronteira onde dados “não confiáveis” entram no seu sistema.

Quando usar o Pydantic?

Instalação e configuração inicial (com uv)

Vamos usar o uv para o setup, já que é o gerenciador de pacotes/ambientes mais rápido e prático do ecossistema Python hoje.

BASH
# Cria um novo projeto (se ainda não tiver um)
uv init pydantic-lab
cd pydantic-lab

# Adiciona o Pydantic como dependência do projeto
uv add pydantic

# (opcional) Adiciona o pydantic-settings, usado para configs via env vars
uv add pydantic-settings
Clique para expandir e ver mais

O uv add já resolve a versão, atualiza o pyproject.toml e cria/atualiza o uv.lock, sem você precisar gerenciar venv manualmente. Para rodar um script do projeto, use:

BASH
uv run python meu_script.py
Clique para expandir e ver mais

Se preferir o fluxo tradicional com pip, também funciona normalmente:

BASH
pip install pydantic
Clique para expandir e ver mais

Para conferir a versão instalada:

BASH
uv run python -c "import pydantic; print(pydantic.VERSION)"
Clique para expandir e ver mais

Introdução aos modelos básicos (BaseModel)

O ponto de partida de praticamente tudo no Pydantic é a classe BaseModel. Você a herda para criar seus próprios modelos, declarando os campos como atributos de classe com type hints:

PYTHON
from pydantic import BaseModel


class Usuario(BaseModel):
    nome: str
    idade: int
    email: str
    ativo: bool = True  # campo com valor padrão -> não é obrigatório
Clique para expandir e ver mais

Alguns pontos importantes já nesse exemplo simples:

Criando uma instância:

PYTHON
usuario = Usuario(nome="Alison", idade=34, email="alison@example.com")
print(usuario)
#> nome='Alison' idade=34 email='alison@example.com' ativo=True
Clique para expandir e ver mais

Validação automática de tipos com Pydantic

A cada vez que você instancia um modelo, o Pydantic roda a validação de todos os campos automaticamente - sem você precisar chamar nenhum método explícito para isso. Vamos ver isso na prática:

PYTHON
from pydantic import BaseModel, ValidationError


class Usuario(BaseModel):
    nome: str
    idade: int
    email: str


# Dados corretos
usuario_ok = Usuario(nome="Maria", idade=30, email="maria@example.com")
print(usuario_ok)
#> nome='Maria' idade=30 email='maria@example.com'

# Dados incorretos
try:
    usuario_invalido = Usuario(nome="João", idade="trinta", email="joao@example.com")
except ValidationError as e:
    print(e)
Clique para expandir e ver mais

Saída do erro (resumida):

TEXT
1 validation error for Usuario
idade
  Input should be a valid integer, unable to parse string as an integer
  [type=int_parsing, input_value='trinta', input_type=str]
Clique para expandir e ver mais

Repare em dois detalhes importantes:

  1. O Pydantic levanta ValidationError (não um ValueError genérico) - uma exceção própria que carrega uma lista estruturada de todos os erros encontrados, campo a campo.
  2. A mensagem já indica exatamente qual campo falhou, por quê e qual foi o valor recebido - ótimo tanto para debugging quanto para devolver como resposta de erro em uma API.

Trabalhando com dados válidos e inválidos

Coerção de tipos (dados “válidos” com conversão)

Por padrão (modo “lax”), o Pydantic tenta converter tipos compatíveis automaticamente, em vez de rejeitar de cara:

PYTHON
usuario = Usuario(nome="Ana", idade="25", email="ana@example.com")
print(usuario)
#> nome='Ana' idade=25 email='ana@example.com'
print(type(usuario.idade))
#> <class 'int'>
Clique para expandir e ver mais

Mesmo recebendo "25" como string, o campo idade é convertido para int. Isso é extremamente útil quando os dados vêm de fontes como formulários HTML, query strings ou variáveis de ambiente, onde tudo naturalmente chega como texto.

Erros de validação (dados realmente inválidos)

Quando a conversão não é possível ou segura, o Pydantic recusa o dado e lista todos os problemas de uma vez:

PYTHON
from pydantic import BaseModel, ValidationError


class Usuario(BaseModel):
    nome: str
    idade: int
    email: str


try:
    usuario = Usuario(nome=123, idade=25.5, email=None)
except ValidationError as e:
    print(e)
Clique para expandir e ver mais
TEXT
3 validation errors for Usuario
nome
  Input should be a valid string [type=string_type, input_value=123, input_type=int]
idade
  Input should be a valid integer, got a number with a fractional part [type=int_from_float, input_value=25.5, input_type=float]
email
  Input should be a valid string [type=string_type, input_value=None, input_type=NoneType]
Clique para expandir e ver mais

Note que o Pydantic não para no primeiro erro - ele coleta todos os erros de validação do modelo de uma vez, o que é bem melhor do que corrigir um campo por vez em ciclos de tentativa e erro.

Modo estrito (strict)

Se você não quer nenhuma coerção automática - por exemplo, para não aceitar "25" como idade - o Pydantic também oferece um modo estrito, tanto por campo quanto por modelo inteiro. Isso é útil quando os dados já vêm de uma fonte 100% confiável (como outro serviço interno que também usa Pydantic) e qualquer divergência de tipo deve ser tratada como bug, não como algo a “consertar” silenciosamente.

Serialização e desserialização de dados JSON

Essa é, na prática, uma das funcionalidades mais usadas no dia a dia de quem constrói APIs.

Serialização (modelo → JSON)

PYTHON
usuario = Usuario(nome="Carlos", idade=40, email="carlos@example.com")

# Para dicionário Python
print(usuario.model_dump())
#> {'nome': 'Carlos', 'idade': 40, 'email': 'carlos@example.com'}

# Direto para uma string JSON
print(usuario.model_dump_json())
#> {"nome":"Carlos","idade":40,"email":"carlos@example.com"}
Clique para expandir e ver mais

model_dump() retorna um dict Python; model_dump_json() já retorna a string JSON pronta para a resposta HTTP.

Desserialização (JSON → modelo)

PYTHON
dados_json = '{"nome": "Luiza", "idade": 22, "email": "luiza@example.com"}'

usuario = Usuario.model_validate_json(dados_json)
print(usuario)
#> nome='Luiza' idade=22 email='luiza@example.com'
Clique para expandir e ver mais

Se preferir partir de um dict já em memória (por exemplo, o corpo de uma requisição já parseado), use model_validate():

PYTHON
dados_dict = {"nome": "Pedro", "idade": 28, "email": "pedro@example.com"}
usuario = Usuario.model_validate(dados_dict)
Clique para expandir e ver mais

💡 Nas versões antigas do Pydantic (V1) você provavelmente viu parse_raw() e .dict(). Esses métodos ainda existem por compatibilidade, mas estão deprecados - na V2, o padrão é model_validate_json(), model_validate(), model_dump() e model_dump_json().

Um pouco mais: Field, tipos opcionais e valores padrão

Antes de partir para o caso prático, vale conhecer o Field, usado para adicionar restrições e metadados aos campos:

PYTHON
from typing import Optional
from pydantic import BaseModel, Field


class Produto(BaseModel):
    nome: str = Field(min_length=2, max_length=100)
    preco: float = Field(gt=0, description="Preço em reais, deve ser positivo")
    estoque: int = Field(default=0, ge=0)
    descricao: Optional[str] = None
Clique para expandir e ver mais

Caso prático (cenário real): validando o payload de um webhook

Um cenário extremamente comum no dia a dia de backend: você expõe um endpoint que recebe webhooks de um serviço externo (gateway de pagamento, CI/CD, sistema de terceiros) e precisa validar o payload antes de processar qualquer coisa. Vamos montar esse cenário com FastAPI + Pydantic, validando o evento de “pedido criado” de um sistema de e-commerce:

PYTHON
from datetime import datetime
from typing import Literal

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, EmailStr, Field, ValidationError


class ItemPedido(BaseModel):
    sku: str = Field(min_length=1)
    quantidade: int = Field(gt=0)
    preco_unitario: float = Field(gt=0)


class WebhookPedidoCriado(BaseModel):
    evento: Literal["pedido.criado"]
    pedido_id: str
    cliente_email: EmailStr
    itens: list[ItemPedido]
    criado_em: datetime
    valor_total: float = Field(gt=0)


app = FastAPI()


@app.post("/webhooks/pedidos")
def receber_webhook(payload: WebhookPedidoCriado):
    # Se chegou até aqui, o Pydantic já validou:
    # - o tipo de cada campo
    # - que "evento" é exatamente "pedido.criado"
    # - que o email é um email válido (EmailStr)
    # - que cada item tem quantidade e preço positivos
    # - que a data está em formato ISO 8601 válido
    total_calculado = sum(item.quantidade * item.preco_unitario for item in payload.itens)

    if abs(total_calculado - payload.valor_total) > 0.01:
        raise HTTPException(status_code=422, detail="valor_total não confere com os itens")

    return {"status": "processado", "pedido_id": payload.pedido_id}
Clique para expandir e ver mais

O que ganhamos aqui, na prática:

Testando localmente com curl:

BASH
curl -X POST http://localhost:8000/webhooks/pedidos \
  -H "Content-Type: application/json" \
  -d '{
    "evento": "pedido.criado",
    "pedido_id": "PED-001",
    "cliente_email": "cliente@example.com",
    "itens": [{"sku": "ABC123", "quantidade": 2, "preco_unitario": 49.9}],
    "criado_em": "2026-08-04T10:00:00",
    "valor_total": 99.8
  }'
Clique para expandir e ver mais

Conclusão

Nesta primeira parte você viu:

Na Parte 2 desta série vamos aprofundar em tópicos intermediários e avançados: validators customizados (field_validator e model_validator), model_config, modelos aninhados e recursivos, Annotated com validação customizada, pydantic-settings para configuração via variáveis de ambiente, e algumas notas sobre performance - sempre com outro caso prático real ao final.

Iniciar busca

Digite palavras-chave para buscar

↑↓
ESC
⌘K Atalho