Python

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

12 min de leitura

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

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

<h2>O que é Pydantic e por que você precisa dominar</h2>

<p>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.</p>

<p>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 <em>na entrada</em>, gerando erros claros que facilitam o debug e a manutenção.</p>

<p>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.</p>

<h2>Validação Básica com Modelos</h2>

<h3>Estrutura Fundamental de um Modelo Pydantic</h3>

<p>Um modelo Pydantic é simplesmente uma classe que herda de <code>BaseModel</code>. 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.</p>

<pre><code class="language-python">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=&quot;João Silva&quot;, email=&quot;joao@example.com&quot;, idade=30)

print(usuario)

Output: id=1 nome=&#039;João Silva&#039; email=&#039;joao@example.com&#039; idade=30 ativo=True

Dados inválidos — tipo errado

try:

usuario_invalido = Usuario(id=&quot;abc&quot;, nome=&quot;Maria&quot;, email=&quot;maria@example.com&quot;)

except ValidationError as e:

print(e)

Mostra exatamente qual campo falhou e por quê</code></pre>

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

<h3>Convertendo e Acessando Dados</h3>

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

<pre><code class="language-python">class Produto(BaseModel):

nome: str

preco: float

estoque: int

String convertida para float automaticamente

produto = Produto(nome=&quot;Notebook&quot;, preco=&quot;1299.99&quot;, estoque=&quot;5&quot;)

print(produto.preco) # Output: 1299.99 (float)

print(produto.estoque) # Output: 5 (int)

Acessar como dicionário

print(produto.model_dump())

Output: {&#039;nome&#039;: &#039;Notebook&#039;, &#039;preco&#039;: 1299.99, &#039;estoque&#039;: 5}

Acessar como JSON string

print(produto.model_dump_json())

Output: &#039;{&quot;nome&quot;:&quot;Notebook&quot;,&quot;preco&quot;:1299.99,&quot;estoque&quot;:5}&#039;</code></pre>

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

<h2>Validação Avançada e Regras Customizadas</h2>

<h3>Field Validators e Constraints</h3>

<p>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 <code>Field</code> para restrições básicas e <code>field_validator</code> para lógica customizada.</p>

<pre><code class="language-python">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(&#039;telefone&#039;)

@classmethod

def validar_telefone(cls, v):

if not re.match(r&#039;^\d{10,11}$&#039;, v):

raise ValueError(&#039;Telefone deve ter 10 ou 11 dígitos&#039;)

return v

@field_validator(&#039;usuario&#039;)

@classmethod

def validar_usuario(cls, v):

if not re.match(r&#039;^[a-zA-Z0-9_]+$&#039;, v):

raise ValueError(&#039;Usuário deve conter apenas letras, números e underscore&#039;)

return v

Válido

conta = Conta(usuario=&quot;joao_silva&quot;, senha=&quot;senhaForte123&quot;, saldo=100.50, telefone=&quot;11999999999&quot;)

print(conta)

Inválido — telefone errado

try:

Conta(usuario=&quot;maria&quot;, senha=&quot;pass123456&quot;, saldo=50.0, telefone=&quot;123&quot;)

except ValidationError as e:

print(e.errors())</code></pre>

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

<h3>Model Validators e Dependências Entre Campos</h3>

<p>Às vezes você precisa validar relacionamentos entre múltiplos campos. Pydantic oferece <code>model_validator</code> para isso.</p>

<pre><code class="language-python">from pydantic import BaseModel, model_validator

from datetime import date

class Intervalo(BaseModel):

data_inicio: date

data_fim: date

@model_validator(mode=&#039;after&#039;)

def validar_intervalo(self):

if self.data_fim &lt;= self.data_inicio:

raise ValueError(&#039;Data fim deve ser posterior à data início&#039;)

return self

Inválido — fim antes do início

try:

Intervalo(data_inicio=&quot;2024-12-31&quot;, data_fim=&quot;2024-01-01&quot;)

except ValidationError as e:

print(e)</code></pre>

<p>O parâmetro <code>mode=&#039;after&#039;</code> significa que a validação ocorre <em>depois</em> que todos os campos foram validados individualmente. Isso permite comparar campos e garantir consistência do modelo como um todo.</p>

<h2>Schemas Complexos e Composição</h2>

<h3>Modelos Aninhados (Nested Models)</h3>

<p>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.</p>

<pre><code class="language-python">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 = {

&quot;id&quot;: 1,

&quot;cliente&quot;: &quot;João Silva&quot;,

&quot;itens&quot;: [

{&quot;nome&quot;: &quot;Notebook&quot;, &quot;quantidade&quot;: 1, &quot;preco_unitario&quot;: 3000.0},

{&quot;nome&quot;: &quot;Mouse&quot;, &quot;quantidade&quot;: 2, &quot;preco_unitario&quot;: 50.0}

],

&quot;desconto&quot;: 0.1

}

pedido = Pedido(**dados)

print(pedido)

print(f&quot;Total: R$ {pedido.total:.2f}&quot;)</code></pre>

<p>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.</p>

<h3>Union Types e Modelos Polimórficos</h3>

<p>Às vezes um campo pode ser um de vários tipos. Pydantic suporta <code>Union</code> para modelar escolhas.</p>

<pre><code class="language-python">from pydantic import BaseModel, Field

from typing import Union, Literal

class Email(BaseModel):

tipo: Literal[&#039;email&#039;] = &#039;email&#039;

endereco: str

class Telefone(BaseModel):

tipo: Literal[&#039;telefone&#039;] = &#039;telefone&#039;

numero: str

class Contato(BaseModel):

meio: Union[Email, Telefone] = Field(discriminator=&#039;tipo&#039;)

Via email

contato1 = Contato(meio={&quot;tipo&quot;: &quot;email&quot;, &quot;endereco&quot;: &quot;joao@example.com&quot;})

print(contato1.meio)

Via telefone

contato2 = Contato(meio={&quot;tipo&quot;: &quot;telefone&quot;, &quot;numero&quot;: &quot;11999999999&quot;})

print(contato2.meio)</code></pre>

<p>O parâmetro <code>discriminator=&#039;tipo&#039;</code> instrui Pydantic a usar o campo <code>tipo</code> para decidir qual modelo usar. Isso torna a seleção mais eficiente e o código mais legível.</p>

<h2>Settings e Configuração de Aplicações</h2>

<h3>Usando BaseSettings para Variáveis de Ambiente</h3>

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

<pre><code class="language-python">from pydantic_settings import BaseSettings

from typing import Optional

class AppConfig(BaseSettings):

app_name: str = &quot;Minha Aplicação&quot;

debug: bool = False

database_url: str

api_key: str

max_retries: int = 3

log_level: str = &quot;INFO&quot;

class Config:

env_file = &quot;.env&quot;

env_file_encoding = &#039;utf-8&#039;

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</code></pre>

<p>O arquivo <code>.env</code> 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.</p>

<h3>Validação de Settings e Defaults Inteligentes</h3>

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

<pre><code class="language-python">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(&#039;port&#039;)

@classmethod

def validar_porta(cls, v):

if v &lt; 1 or v &gt; 65535:

raise ValueError(&#039;Porta deve estar entre 1 e 65535&#039;)

return v

@field_validator(&#039;host&#039;)

@classmethod

def validar_host(cls, v):

if not v or len(v) == 0:

raise ValueError(&#039;Host não pode estar vazio&#039;)

return v

class Config:

env_file = &quot;.env&quot;

Variáveis de ambiente

os.environ[&#039;HOST&#039;] = &#039;localhost&#039;

os.environ[&#039;USER&#039;] = &#039;admin&#039;

os.environ[&#039;PASSWORD&#039;] = &#039;secret123&#039;

os.environ[&#039;DATABASE&#039;] = &#039;producao&#039;

os.environ[&#039;PORT&#039;] = &#039;5432&#039;

config = DatabaseConfig()

print(f&quot;Conectando em {config.host}:{config.port}&quot;)</code></pre>

<p>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.</p>

<h2>Conclusão</h2>

<p>Pydantic oferece três pilares principais para dominar: <strong>Validação automática</strong> através de type hints elimina código manual repetitivo; <strong>Schemas complexos</strong> permitem modelar dados reais com aninhamento, composição e polimorfismo; <strong>Settings configuráveis</strong> 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.</p>

<h2>Referências</h2>

<ul>

<li>https://docs.pydantic.dev/latest/ — Documentação oficial do Pydantic v2</li>

<li>https://fastapi.tiangolo.com/ — Framework web que usa Pydantic internamente</li>

<li>https://pydantic-docs.helpmanual.io/usage/validators/ — Guia detalhado de validadores</li>

<li>https://realpython.com/python-pydantic/ — Tutorial prático da Real Python</li>

<li>https://github.com/pydantic/pydantic — Repositório oficial no GitHub</li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em Python

Guia Completo de Funções em Python: Definição, *args, **kwargs e Escopo
Guia Completo de Funções em Python: Definição, *args, **kwargs e Escopo

Introdução: Entendendo Funções em Python Funções são blocos de código reutili...

Dominando asyncio Avançado em Python: Semáforos, Locks e Padrões de Concorrência em Projetos Reais
Dominando asyncio Avançado em Python: Semáforos, Locks e Padrões de Concorrência em Projetos Reais

Introdução: O Problema da Concorrência Controlada Quando trabalhamos com em P...

Django em Python: MVT, ORM, Admin e Estrutura de Projetos: Do Básico ao Avançado
Django em Python: MVT, ORM, Admin e Estrutura de Projetos: Do Básico ao Avançado

Entendendo o MVT: A Arquitetura do Django Django segue um padrão arquitetural...