Pandera é uma biblioteca para declarar e executar contratos sobre dados tabulares. Em vez de espalhar verificações como if "preco" in df.columns pelo código, você descreve colunas, tipos, valores permitidos e regras entre campos em um schema reutilizável. A resposta curta é: use Pandera na entrada e na saída das etapas críticas para transformar erros silenciosos de dados em falhas claras, localizadas e testáveis.
Ela complementa o pandas, não o substitui. O pandas carrega, transforma e agrega; Pandera confirma se o DataFrame recebido ou produzido corresponde ao contrato esperado. Isso é útil em ETL, análises, notebooks que viraram produção, APIs e treinamento de modelos. Se DataFrames ainda são novidade, comece pelo guia de pandas para análise de dados.
Instalação e primeiro schema
Crie um ambiente virtual com venv e instale Pandera com suporte a pandas. O extra evita depender de componentes opcionais por acaso:
python -m pip install "pandera[pandas]" pandas pytest
Há duas APIs comuns. DataFrameSchema monta o contrato com objetos em tempo de execução; DataFrameModel usa anotações de tipo e costuma ficar mais legível em projetos. A documentação oficial do Pandera apresenta os backends e recursos disponíveis. Neste tutorial usaremos o modelo tipado:
import pandas as pd
import pandera.pandas as pa
from pandera.typing import Series
class VendaSchema(pa.DataFrameModel):
pedido_id: Series[int] = pa.Field(unique=True, ge=1)
produto: Series[str] = pa.Field(str_length={"min_value": 1})
quantidade: Series[int] = pa.Field(gt=0)
preco_unitario: Series[float] = pa.Field(ge=0)
status: Series[str] = pa.Field(isin=["pago", "enviado", "cancelado"])
class Config:
strict = True
coerce = True
dados = pd.DataFrame({
"pedido_id": [101, 102],
"produto": ["Teclado", "Mouse"],
"quantidade": [1, 2],
"preco_unitario": [199.90, 79.50],
"status": ["pago", "enviado"],
})
validado = VendaSchema.validate(dados)
print(validado.dtypes)
strict = True rejeita colunas desconhecidas. Isso é adequado quando uma mudança inesperada deve interromper o pipeline; remova a opção se colunas extras forem aceitáveis. coerce = True tenta converter valores para os tipos declarados. Coerção é conveniência, não correção: a string "abc" continua inválida como inteiro, e uma conversão bem-sucedida não garante que o significado esteja certo.
Colunas, checks e valores nulos
Field cobre regras frequentes: gt, ge, lt, le, isin, unique, limites de tamanho e correspondência por expressão regular. Uma coluna não aceita nulos por padrão. Para permitir ausência, declare nullable=True; isso é diferente de tornar a coluna opcional. nullable aceita valores vazios dentro de uma coluna presente, enquanto required=False na API de schemas permite que a coluna nem exista.
Regras que envolvem mais de uma coluna pertencem ao DataFrame inteiro. Por exemplo, pedidos cancelados podem ter total zero, mas os demais precisam gerar receita positiva:
class VendaComTotal(VendaSchema):
total: Series[float] = pa.Field(ge=0)
@pa.dataframe_check
def total_coerente(cls, df: pd.DataFrame) -> Series[bool]:
calculado = df["quantidade"] * df["preco_unitario"]
return (df["status"] == "cancelado") | df["total"].eq(calculado)
O método devolve uma série booleana, uma resposta por linha. Prefira operações vetorizadas a loops: elas expressam melhor a regra e aproveitam o funcionamento do pandas. Anotações também ajudam editores e revisores; aprofunde esse tema no artigo de type hints em Python.
Projeto prático: pipeline de vendas
Vamos separar leitura, transformação e validação. O arquivo vendas.py recebe um CSV, normaliza texto, calcula o total e entrega apenas dados aprovados:
from pathlib import Path
import pandas as pd
import pandera.pandas as pa
from pandera.typing import DataFrame, Series
class VendaEntrada(pa.DataFrameModel):
pedido_id: Series[int] = pa.Field(unique=True, ge=1)
produto: Series[str] = pa.Field(str_length={"min_value": 1})
quantidade: Series[int] = pa.Field(gt=0)
preco_unitario: Series[float] = pa.Field(ge=0)
status: Series[str] = pa.Field(isin=["pago", "enviado", "cancelado"])
class Config:
strict = True
coerce = True
class VendaSaida(VendaEntrada):
total: Series[float] = pa.Field(ge=0)
@pa.check_types
def transformar(df: DataFrame[VendaEntrada]) -> DataFrame[VendaSaida]:
resultado = df.copy()
resultado["produto"] = resultado["produto"].str.strip()
resultado["total"] = (
resultado["quantidade"] * resultado["preco_unitario"]
).where(resultado["status"] != "cancelado", 0.0)
return resultado
def carregar(caminho: str | Path) -> DataFrame[VendaEntrada]:
bruto = pd.read_csv(caminho)
return VendaEntrada.validate(bruto, lazy=True)
O decorator check_types valida argumentos e retorno anotados. Assim, uma transformação não pode prometer VendaSaida e esquecer total. Ainda vale manter funções pequenas: veja como organizar essa lógica no guia de funções em Python. Em conjuntos enormes, validar tudo pode custar tempo; amostragem serve para exploração, mas contratos críticos normalmente devem examinar o conjunto completo.
Testes de dados válidos e inválidos
Schema também é código e merece testes. O primeiro cenário protege o caminho feliz; o segundo comprova que uma quantidade inválida não passa:
import pandas as pd
import pandera.errors
import pytest
from vendas import VendaEntrada, transformar
def venda_valida() -> pd.DataFrame:
return pd.DataFrame({
"pedido_id": [1],
"produto": ["Caderno"],
"quantidade": [3],
"preco_unitario": [12.5],
"status": ["pago"],
})
def test_transformar_calcula_total():
saida = transformar(VendaEntrada.validate(venda_valida()))
assert saida.loc[0, "total"] == 37.5
def test_quantidade_zero_falha():
df = venda_valida()
df.loc[0, "quantidade"] = 0
with pytest.raises(pandera.errors.SchemaError):
VendaEntrada.validate(df)
Execute com pytest -q. Para ampliar a suíte, consulte o tutorial de testes automatizados com pytest. Teste limites, nulos, duplicidade, categoria desconhecida, coluna ausente e coluna adicional, não apenas um exemplo genérico.
Erros úteis com validação lazy
Sem lazy=True, Pandera interrompe na primeira violação. Isso é rápido, porém ruim para um arquivo enviado por usuário, que teria de corrigir um erro por vez. O modo lazy reúne as falhas em SchemaErrors:
import pandera.errors
try:
vendas = VendaEntrada.validate(dados, lazy=True)
except pandera.errors.SchemaErrors as exc:
falhas = exc.failure_cases[
["schema_context", "column", "check", "failure_case", "index"]
]
falhas.to_csv("vendas_rejeitadas.csv", index=False)
raise SystemExit("Arquivo inválido; consulte vendas_rejeitadas.csv") from exc
Capture a exceção específica, registre contexto e encerre a etapa. Não continue com o DataFrame bruto depois da falha, pois isso anula o contrato. Para estratégias de exceção e mensagens, leia tratamento de erros com try/except. Evite também registrar linhas completas quando houver dados pessoais.
Boas práticas e armadilhas
Coloque schemas perto do domínio, versionados junto ao código. Dê nomes de negócio às classes, valide cedo na fronteira e novamente após transformações que alterem a estrutura. Separe regras estruturais de decisões que exigem banco de dados ou serviços externos. Um schema gigantesco e acoplado a tudo fica difícil de evoluir.
Não confunda tipo com semântica. Um CPF pode ser str e ainda ser inválido; uma data pode ser parseável e estar fora do período aceito. Tampouco use coerção para esconder uma fonte inconsistente. Observe as falhas e corrija o produtor quando possível. Em pipelines contínuos, acompanhe quantidade e motivo das rejeições para detectar mudanças antes que usuários percebam.
Perguntas frequentes
Pandera substitui pandas?
Não. Pandas manipula DataFrames; Pandera declara e verifica contratos para esses objetos. As bibliotecas trabalham juntas.
Pandera substitui testes com pytest?
Não. O schema executa validações; pytest confirma que schemas e transformações se comportam corretamente em cenários conhecidos.
Devo usar DataFrameSchema ou DataFrameModel?
Use o modelo tipado quando anotações melhorarem a API do projeto. Use DataFrameSchema quando precisar montar regras dinamicamente. Ambos são válidos.
Quando usar lazy=True?
Use quando o consumidor precisa receber uma lista completa de problemas, como na importação de CSV. Para falhar imediatamente em etapas internas, o padrão pode bastar.
Conclusão
Pandera torna explícito aquilo que um pipeline espera: nomes, tipos, limites e relações entre colunas. Comece validando uma fronteira importante, adicione testes para casos válidos e inválidos e trate relatórios lazy sem deixar dados rejeitados avançarem. Esse pequeno contrato reduz depuração, documenta decisões e dá segurança para alterar transformações sem aceitar regressões silenciosas.