Voltar ao Glossario
Glossario Python

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:

Aspectov1v2
PerformancePython puroNúcleo em Rust, até 50x mais rápido
Validator@validator@field_validator e @model_validator
Configuraçãoclasse Configmodel_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 @classmethod nos validators: @field_validator exige @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-settings para 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