Pandera es una biblioteca para declarar y ejecutar contratos sobre datos tabulares. En lugar de repartir comprobaciones como if "precio" in df.columns por toda la aplicación, permite describir columnas, tipos, valores admitidos y reglas entre campos en un esquema reutilizable. La respuesta breve es: usa Pandera en la entrada y salida de las etapas críticas para convertir defectos silenciosos en fallos claros, localizados y comprobables.

Pandera complementa pandas, no lo reemplaza. Pandas carga, transforma y agrega; Pandera confirma que el DataFrame recibido o producido respeta el contrato. Esta separación resulta útil en procesos ETL, análisis, notebooks llevados a producción, APIs y aprendizaje automático. Si todavía no conoces los DataFrames, empieza con nuestra guía de pandas para análisis de datos.

Instalación y primer esquema

Crea un entorno virtual con venv e instala Pandera con soporte para pandas. Declarar el extra impide depender accidentalmente de componentes opcionales:

python -m pip install "pandera[pandas]" pandas pytest

Existen dos APIs habituales. DataFrameSchema construye el contrato con objetos en tiempo de ejecución; DataFrameModel emplea anotaciones de tipo y suele ser más legible en un proyecto. La documentación oficial de Pandera explica los backends y recursos disponibles. Aquí utilizaremos el modelo tipado:

import pandas as pd
import pandera.pandas as pa
from pandera.typing import Series

class VentaSchema(pa.DataFrameModel):
    pedido_id: Series[int] = pa.Field(unique=True, ge=1)
    producto: Series[str] = pa.Field(str_length={"min_value": 1})
    cantidad: Series[int] = pa.Field(gt=0)
    precio_unitario: Series[float] = pa.Field(ge=0)
    estado: Series[str] = pa.Field(isin=["pagado", "enviado", "cancelado"])

    class Config:
        strict = True
        coerce = True

datos = pd.DataFrame({
    "pedido_id": [101, 102],
    "producto": ["Teclado", "Ratón"],
    "cantidad": [1, 2],
    "precio_unitario": [199.90, 79.50],
    "estado": ["pagado", "enviado"],
})

validado = VentaSchema.validate(datos)
print(validado.dtypes)

strict = True rechaza columnas desconocidas. Es apropiado cuando un cambio estructural inesperado debe detener el pipeline; elimínalo si se permiten columnas adicionales. coerce = True intenta convertir los valores a los tipos declarados. La coerción es una ayuda, no una garantía: "abc" no puede convertirse en entero y una conversión exitosa tampoco confirma el significado del dato.

Columnas, checks y valores nulos

Field cubre restricciones frecuentes: gt, ge, lt, le, isin, unique, longitud de texto y expresiones regulares. Las columnas no aceptan nulos por defecto. Indica nullable=True cuando una celda vacía sea válida. No es igual que una columna opcional: nullable permite nulos dentro de una columna presente, mientras required=False en la API de esquemas permite que la columna completa no exista.

Las reglas que involucran varias columnas pertenecen al DataFrame. Por ejemplo, una venta cancelada puede tener total cero, pero las demás deben conservar el importe calculado:

class VentaConTotal(VentaSchema):
    total: Series[float] = pa.Field(ge=0)

    @pa.dataframe_check
    def total_coherente(cls, df: pd.DataFrame) -> Series[bool]:
        calculado = df["cantidad"] * df["precio_unitario"]
        return (df["estado"] == "cancelado") | df["total"].eq(calculado)

El método devuelve un booleano por fila. Conviene utilizar operaciones vectorizadas en vez de bucles: expresan directamente la regla y aprovechan el funcionamiento de pandas. Las anotaciones también orientan al editor y al revisor; nuestra guía de type hints en Python desarrolla este recurso.

Proyecto práctico: pipeline de ventas

Separaremos carga, transformación y validación. El módulo ventas.py recibe un CSV, normaliza productos, calcula totales y devuelve únicamente datos aprobados:

from pathlib import Path

import pandas as pd
import pandera.pandas as pa
from pandera.typing import DataFrame, Series

class VentaEntrada(pa.DataFrameModel):
    pedido_id: Series[int] = pa.Field(unique=True, ge=1)
    producto: Series[str] = pa.Field(str_length={"min_value": 1})
    cantidad: Series[int] = pa.Field(gt=0)
    precio_unitario: Series[float] = pa.Field(ge=0)
    estado: Series[str] = pa.Field(isin=["pagado", "enviado", "cancelado"])

    class Config:
        strict = True
        coerce = True

class VentaSalida(VentaEntrada):
    total: Series[float] = pa.Field(ge=0)

@pa.check_types
def transformar(df: DataFrame[VentaEntrada]) -> DataFrame[VentaSalida]:
    resultado = df.copy()
    resultado["producto"] = resultado["producto"].str.strip()
    resultado["total"] = (
        resultado["cantidad"] * resultado["precio_unitario"]
    ).where(resultado["estado"] != "cancelado", 0.0)
    return resultado

def cargar(ruta: str | Path) -> DataFrame[VentaEntrada]:
    bruto = pd.read_csv(ruta)
    return VentaEntrada.validate(bruto, lazy=True)

El decorador check_types valida entradas y salidas anotadas. Por tanto, una transformación no puede prometer VentaSalida y olvidar silenciosamente total. Mantén las funciones enfocadas; el artículo sobre funciones en Python ayuda a organizar esta lógica. En volúmenes enormes, validar todo tiene un coste. Una muestra sirve para explorar, pero los contratos críticos normalmente deben revisar cada registro entregado.

Pruebas con datos válidos e inválidos

Un esquema es código ejecutable y necesita pruebas. Un caso protege el camino correcto; otro demuestra que una cantidad igual a cero no pasa:

import pandas as pd
import pandera.errors
import pytest

from ventas import VentaEntrada, transformar

def venta_valida() -> pd.DataFrame:
    return pd.DataFrame({
        "pedido_id": [1],
        "producto": ["Cuaderno"],
        "cantidad": [3],
        "precio_unitario": [12.5],
        "estado": ["pagado"],
    })

def test_transformar_calcula_total():
    salida = transformar(VentaEntrada.validate(venta_valida()))
    assert salida.loc[0, "total"] == 37.5

def test_cantidad_cero_falla():
    df = venta_valida()
    df.loc[0, "cantidad"] = 0

    with pytest.raises(pandera.errors.SchemaError):
        VentaEntrada.validate(df)

Ejecuta pytest -q. Si necesitas fixtures o parametrización, consulta la guía de pruebas automatizadas con pytest. Incluye límites, nulos, identificadores duplicados, categorías desconocidas, una columna ausente y otra inesperada. Los tests deben describir el contrato real, no limitarse a provocar un error cualquiera.

Errores útiles con validación lazy

Sin lazy=True, Pandera se detiene ante la primera infracción. Es rápido, pero incómodo para quien carga un archivo y tendría que corregir un problema cada vez. El modo lazy reúne los fallos en SchemaErrors:

import pandera.errors

try:
    ventas = VentaEntrada.validate(datos, lazy=True)
except pandera.errors.SchemaErrors as exc:
    fallos = exc.failure_cases[
        ["schema_context", "column", "check", "failure_case", "index"]
    ]
    fallos.to_csv("ventas_rechazadas.csv", index=False)
    raise SystemExit("Archivo inválido; revisa ventas_rechazadas.csv") from exc

Captura la excepción específica, conserva contexto útil y detén la etapa. Nunca continúes con el DataFrame original después del fallo, porque eliminarías el beneficio del contrato. La guía de tratamiento de errores con try/except explica cómo delimitar excepciones. Evita registrar filas completas si contienen información personal.

Buenas prácticas y errores comunes

Mantén los esquemas cerca del dominio y bajo el mismo control de versiones que el código. Valida pronto en las fronteras y otra vez después de transformaciones estructurales. Separa reglas autocontenidas de decisiones que requieren bases de datos o servicios externos. Un esquema enorme conectado con todas las responsabilidades se vuelve difícil de entender y modificar.

No confundas validez de tipo con validez semántica. Un identificador puede ser texto y tener un formato incorrecto; una fecha puede interpretarse y quedar fuera del período admitido. Tampoco uses coerción para esconder una fuente inestable. Observa los fallos y corrige al productor cuando sea posible. En procesos recurrentes, mide cantidades y motivos de rechazo para descubrir cambios antes de que dañen informes.

Preguntas frecuentes

¿Pandera reemplaza pandas?

No. Pandas manipula DataFrames; Pandera declara y comprueba contratos para ellos. Resuelven problemas diferentes y complementarios.

¿Pandera reemplaza las pruebas con pytest?

No. El esquema ejecuta validaciones. Pytest verifica que esquemas y transformaciones se comporten como esperas ante escenarios representativos.

¿Debo elegir DataFrameSchema o DataFrameModel?

Elige el modelo tipado cuando las anotaciones mejoren la API. Usa DataFrameSchema para reglas construidas dinámicamente. Ambos enfoques son válidos.

¿Cuándo conviene usar lazy=True?

Cuando el consumidor necesita la lista completa de problemas, como al importar un CSV. Para una etapa interna controlada puede ser mejor fallar inmediatamente.

Conclusión

Pandera vuelve explícitas las expectativas del pipeline: nombres, tipos, límites, categorías y relaciones. Comienza por una frontera importante, prueba ejemplos aceptados y rechazados, y procesa los informes lazy sin permitir que avancen registros incorrectos. Un contrato pequeño y preciso reduce depuración, documenta decisiones y permite cambiar transformaciones con mayor seguridad.