Python Admin

O que Todo Dev Deve Saber sobre Pydantic em Python: Validação de Dados, Schemas e Settings Já leu

O que é Pydantic e por que você precisa dominar Pydantic é uma biblioteca Python que oferece validação de dados e configuração de settings através de modelos baseados em type hints. Diferente de outras abordagens, ela utiliza anotações de tipo nativas do Python para definir o schema dos seus dados, tornando o código mais legível e Pythônico. A razão pela qual você deve aprender Pydantic é simples: dados inválidos causam bugs em cascata. Imagine receber um JSON de uma API externa com campos faltando, tipos incorretos ou valores impossíveis. Sem validação apropriada, seu código quebraria em locais imprevisíveis. Pydantic detecta esses problemas na entrada, gerando erros claros que facilitam o debug e a manutenção. Ela é amplamente adotada em projetos FastAPI, sistemas de configuração, processamento de dados científicos e qualquer aplicação que necessite confiabilidade. Ao dominar Pydantic, você dominará um padrão industrial que economiza horas de validação manual. Validação Básica com Modelos Estrutura Fundamental de um Modelo Pydantic Um modelo

O que é Pydantic e por que você precisa dominar

Pydantic é uma biblioteca Python que oferece validação de dados e configuração de settings através de modelos baseados em type hints. Diferente de outras abordagens, ela utiliza anotações de tipo nativas do Python para definir o schema dos seus dados, tornando o código mais legível e Pythônico.

A razão pela qual você deve aprender Pydantic é simples: dados inválidos causam bugs em cascata. Imagine receber um JSON de uma API externa com campos faltando, tipos incorretos ou valores impossíveis. Sem validação apropriada, seu código quebraria em locais imprevisíveis. Pydantic detecta esses problemas na entrada, gerando erros claros que facilitam o debug e a manutenção.

Ela é amplamente adotada em projetos FastAPI, sistemas de configuração, processamento de dados científicos e qualquer aplicação que necessite confiabilidade. Ao dominar Pydantic, você dominará um padrão industrial que economiza horas de validação manual.

Validação Básica com Modelos

Estrutura Fundamental de um Modelo Pydantic

Um modelo Pydantic é simplesmente uma classe que herda de BaseModel. Você define atributos com type hints, e a biblioteca cuida da validação automaticamente. Quando você instancia a classe, Pydantic verifica cada campo contra sua anotação de tipo.

from pydantic import BaseModel, ValidationError
from typing import Optional

class Usuario(BaseModel):
    id: int
    nome: str
    email: str
    idade: Optional[int] = None
    ativo: bool = True

# Dados válidos
usuario = Usuario(id=1, nome="João Silva", email="joao@example.com", idade=30)
print(usuario)
# Output: id=1 nome='João Silva' email='joao@example.com' idade=30 ativo=True

# Dados inválidos — tipo errado
try:
    usuario_invalido = Usuario(id="abc", nome="Maria", email="maria@example.com")
except ValidationError as e:
    print(e)
    # Mostra exatamente qual campo falhou e por quê

O modelo acima define cinco campos. Os campos id, nome e email são obrigatórios. O campo idade é opcional (aceita None) e ativo possui um padrão. Quando você cria uma instância, Pydantic tenta converter e validar cada valor.

Convertendo e Acessando Dados

Pydantic não apenas valida — também converte dados quando apropriado. Se você passa id="42" como string, Pydantic converte para inteiro. Isso torna sua API mais flexível sem comprometer a segurança.

class Produto(BaseModel):
    nome: str
    preco: float
    estoque: int

# String convertida para float automaticamente
produto = Produto(nome="Notebook", preco="1299.99", estoque="5")
print(produto.preco)  # Output: 1299.99 (float)
print(produto.estoque)  # Output: 5 (int)

# Acessar como dicionário
print(produto.model_dump())
# Output: {'nome': 'Notebook', 'preco': 1299.99, 'estoque': 5}

# Acessar como JSON string
print(produto.model_dump_json())
# Output: '{"nome":"Notebook","preco":1299.99,"estoque":5}'

Esse comportamento de coerção é controlado pelo Pydantic. Você pode ser mais rigoroso configurando ConfigDict(str_strip_whitespace=False) ou validadores customizados.

Validação Avançada e Regras Customizadas

Field Validators e Constraints

Nem sempre uma anotação de tipo é suficiente. Às vezes você precisa validar padrões específicos, ranges ou dependências entre campos. Pydantic oferece Field para restrições básicas e field_validator para lógica customizada.

from pydantic import BaseModel, Field, field_validator
import re

class Conta(BaseModel):
    usuario: str = Field(min_length=3, max_length=20)
    senha: str = Field(min_length=8)
    saldo: float = Field(gt=0)  # greater than 0
    telefone: str

    @field_validator('telefone')
    @classmethod
    def validar_telefone(cls, v):
        if not re.match(r'^\d{10,11}$', v):
            raise ValueError('Telefone deve ter 10 ou 11 dígitos')
        return v

    @field_validator('usuario')
    @classmethod
    def validar_usuario(cls, v):
        if not re.match(r'^[a-zA-Z0-9_]+$', v):
            raise ValueError('Usuário deve conter apenas letras, números e underscore')
        return v

# Válido
conta = Conta(usuario="joao_silva", senha="senhaForte123", saldo=100.50, telefone="11999999999")
print(conta)

# Inválido — telefone errado
try:
    Conta(usuario="maria", senha="pass123456", saldo=50.0, telefone="123")
except ValidationError as e:
    print(e.errors())

O Field permite estabelecer restrições declarativas como comprimento mínimo, máximo e comparações. O @field_validator é um decorator que define funções de validação customizadas. Você recebe o valor e pode modificá-lo ou lançar ValueError se inválido.

Model Validators e Dependências Entre Campos

Às vezes você precisa validar relacionamentos entre múltiplos campos. Pydantic oferece model_validator para isso.

from pydantic import BaseModel, model_validator
from datetime import date

class Intervalo(BaseModel):
    data_inicio: date
    data_fim: date

    @model_validator(mode='after')
    def validar_intervalo(self):
        if self.data_fim <= self.data_inicio:
            raise ValueError('Data fim deve ser posterior à data início')
        return self

# Inválido — fim antes do início
try:
    Intervalo(data_inicio="2024-12-31", data_fim="2024-01-01")
except ValidationError as e:
    print(e)

O parâmetro mode='after' significa que a validação ocorre depois que todos os campos foram validados individualmente. Isso permite comparar campos e garantir consistência do modelo como um todo.

Schemas Complexos e Composição

Modelos Aninhados (Nested Models)

Dados reais são complexos. Um pedido contém múltiplos itens, cada um com detalhes próprios. Pydantic permite aninhar modelos, criando estruturas hierárquicas naturais.

from pydantic import BaseModel
from typing import List

class Item(BaseModel):
    nome: str
    quantidade: int
    preco_unitario: float

    @property
    def subtotal(self):
        return self.quantidade * self.preco_unitario

class Pedido(BaseModel):
    id: int
    cliente: str
    itens: List[Item]  # Lista de modelos aninhados
    desconto: float = 0.0

    @property
    def total(self):
        subtotal = sum(item.subtotal for item in self.itens)
        return subtotal * (1 - self.desconto)

# Dados como dicionário aninhado
dados = {
    "id": 1,
    "cliente": "João Silva",
    "itens": [
        {"nome": "Notebook", "quantidade": 1, "preco_unitario": 3000.0},
        {"nome": "Mouse", "quantidade": 2, "preco_unitario": 50.0}
    ],
    "desconto": 0.1
}

pedido = Pedido(**dados)
print(pedido)
print(f"Total: R$ {pedido.total:.2f}")

Pydantic valida recursivamente cada nível. Se um item tem um campo inválido, o erro apontará exatamente onde está o problema. Você também pode usar propriedades para cálculos derivados.

Union Types e Modelos Polimórficos

Às vezes um campo pode ser um de vários tipos. Pydantic suporta Union para modelar escolhas.

from pydantic import BaseModel, Field
from typing import Union, Literal

class Email(BaseModel):
    tipo: Literal['email'] = 'email'
    endereco: str

class Telefone(BaseModel):
    tipo: Literal['telefone'] = 'telefone'
    numero: str

class Contato(BaseModel):
    meio: Union[Email, Telefone] = Field(discriminator='tipo')

# Via email
contato1 = Contato(meio={"tipo": "email", "endereco": "joao@example.com"})
print(contato1.meio)

# Via telefone
contato2 = Contato(meio={"tipo": "telefone", "numero": "11999999999"})
print(contato2.meio)

O parâmetro discriminator='tipo' instrui Pydantic a usar o campo tipo para decidir qual modelo usar. Isso torna a seleção mais eficiente e o código mais legível.

Settings e Configuração de Aplicações

Usando BaseSettings para Variáveis de Ambiente

Aplicações precisam de configuração — chaves de API, URLs de banco de dados, níveis de log. Pydantic oferece BaseSettings para carregar essas configurações de variáveis de ambiente de forma segura.

from pydantic_settings import BaseSettings
from typing import Optional

class AppConfig(BaseSettings):
    app_name: str = "Minha Aplicação"
    debug: bool = False
    database_url: str
    api_key: str
    max_retries: int = 3
    log_level: str = "INFO"

    class Config:
        env_file = ".env"
        env_file_encoding = 'utf-8'

# Se existir .env com:
# DATABASE_URL=postgresql://user:pass@localhost/db
# API_KEY=chave_secreta_xyz
# DEBUG=true

config = AppConfig()
print(config.database_url)
print(config.debug)
print(config.max_retries)  # Usa padrão de 3

O arquivo .env permite manter configurações sensíveis fora do código-fonte. Pydantic carrega automaticamente e valida. Se uma variável obrigatória faltar, você recebe um erro claro na inicialização da aplicação.

Validação de Settings e Defaults Inteligentes

Settings precisam estar corretos desde o início. Pydantic permite usar validadores em BaseSettings para processar valores de ambiente.

from pydantic_settings import BaseSettings
from pydantic import field_validator
import os

class DatabaseConfig(BaseSettings):
    host: str
    port: int = 5432
    user: str
    password: str
    database: str

    @field_validator('port')
    @classmethod
    def validar_porta(cls, v):
        if v < 1 or v > 65535:
            raise ValueError('Porta deve estar entre 1 e 65535')
        return v

    @field_validator('host')
    @classmethod
    def validar_host(cls, v):
        if not v or len(v) == 0:
            raise ValueError('Host não pode estar vazio')
        return v

    class Config:
        env_file = ".env"

# Variáveis de ambiente
os.environ['HOST'] = 'localhost'
os.environ['USER'] = 'admin'
os.environ['PASSWORD'] = 'secret123'
os.environ['DATABASE'] = 'producao'
os.environ['PORT'] = '5432'

config = DatabaseConfig()
print(f"Conectando em {config.host}:{config.port}")

Dessa forma, você tem certeza que sua aplicação iniciará apenas com configurações válidas. Erros de configuração são detectados imediatamente, não em tempo de execução.

Conclusão

Pydantic oferece três pilares principais para dominar: Validação automática através de type hints elimina código manual repetitivo; Schemas complexos permitem modelar dados reais com aninhamento, composição e polimorfismo; Settings configuráveis garantem que sua aplicação comece com valores válidos e seguros. Ao internalizar esses conceitos, você escreverá código mais robusto, mantível e confiável — padrões que separam código amador de código profissional.

Referências

  • https://docs.pydantic.dev/latest/ — Documentação oficial do Pydantic v2
  • https://fastapi.tiangolo.com/ — Framework web que usa Pydantic internamente
  • https://pydantic-docs.helpmanual.io/usage/validators/ — Guia detalhado de validadores
  • https://realpython.com/python-pydantic/ — Tutorial prático da Real Python
  • https://github.com/pydantic/pydantic — Repositório oficial no GitHub

Artigos relacionados