name: pandera
description: >-
Pandera is a Python library for validating DataFrames (pandas, Polars, PySpark,
Dask and others) against declarative schemas with column types, constraints
and custom checks. Use this skill when asked to validate a DataFrame, define a
data contract, add data quality checks to a pipeline or pytest suite, or
report which rows fail validation.
license: Apache-2.0
compatibility: "Python 3.9+; pandas or polars installed through pandera extras"
metadata:
author: terminal-skills
version: "1.1.0"
category: data-ai
repository: https://github.com/unionai-oss/pandera
tags: ["data-validation", "pandas", "polars", "python", "schema"]
Pandera — Data Validation for DataFrames
Overview
Pandera validates DataFrames against schemas you declare as classes (DataFrameModel) or objects (DataFrameSchema). Each column gets a type, constraints (ge, isin, str_matches, unique, nullable) and optional custom checks; whole-frame rules go in @pa.dataframe_check. It works with pandas (pandera.pandas), Polars (pandera.polars), PySpark, Dask, Modin and Ibis. The pandas API lives in pandera.pandas, and the base pandera package no longer installs pandas for you.
Instructions
Schema Definition
Define column types, constraints, and checks:
# schemas/orders.py — Order data validation schema
import pandas as pd
import pandera.pandas as pa
from pandera.typing import Series, DataFrame
class OrderSchema(pa.DataFrameModel):
"""Schema for validated order records (one row per order)."""
order_id: Series[str] = pa.Field(
unique=True,
str_matches=r"^ORD-\d{8}quot;, # Format: ORD-00000001
description="Unique order identifier",
)
customer_id: Series[str] = pa.Field(
nullable=False,
str_length={"min_value": 1, "max_value": 50},
)
amount: Series[float] = pa.Field(
ge=0.01, # Minimum $0.01
le=100_000, # Maximum $100,000 (sanity check)
description="Order total in USD",
)
status: Series[str] = pa.Field(
isin=["pending", "processing", "completed", "cancelled", "refunded"],
)
currency: Series[str] = pa.Field(
isin=["USD", "EUR", "GBP"],
default="USD",
)
created_at: Series[pd.Timestamp] = pa.Field(
nullable=False,
description="Order creation timestamp (UTC)",
)
shipped_at: Series[pd.Timestamp] = pa.Field(
nullable=True, # Not all orders are shipped yet
)
items_count: Series[int] = pa.Field(
ge=1, # At least one item per order
le=100, # Max 100 items
)
# DataFrame-level validation (checks across columns)
@pa.dataframe_check
def shipped_after_created(cls, df: pd.DataFrame) -> Series[bool]:
"""Shipped date must be after creation date (when present)."""
mask = df["shipped_at"].notna()
result = pd.Series(True, index=df.index)
result[mask] = df.loc[mask, "shipped_at"] > df.loc[mask, "created_at"]
return result
@pa.dataframe_check
def completed_must_be_shipped(cls, df: pd.DataFrame) -> Series[bool]:
"""Completed orders must have a shipped date."""
completed = df["status"] == "completed"
return ~completed | df["shipped_at"].notna()
class Config:
strict = True # Reject extra columns not in schema
coerce = True # Auto-coerce types (str → int, etc.)
name = "OrderSchema"
description = "Validated order records for the analytics pipeline"
Using Schemas in Pipelines
# pipelines/orders.py — Data pipeline with validation
import pandas as pd
import pandera.pandas as pa
from pandera.typing import DataFrame
from schemas.orders import OrderSchema
@pa.check_types # Validates return type at runtime
def load_orders(filepath: str) -> DataFrame[OrderSchema]:
"""Load order data from CSV; raises pa.errors.SchemaError if invalid."""
df = pd.read_csv(filepath, parse_dates=["created_at", "shipped_at"])
return df # Auto-validated by @check_types
def process_orders(orders: DataFrame[OrderSchema]) -> pd.DataFrame:
"""Process validated orders into daily revenue summary."""
return (
orders
.query("status == 'completed'")
.groupby(orders["created_at"].dt.date)
.agg(
revenue=("amount", "sum"),
order_count=("order_id", "count"),
avg_items=("items_count", "mean"),
)
.reset_index()
)
# Usage
try:
orders = load_orders("data/orders_2026_03.csv")
summary = process_orders(orders)
print(f"Processed {len(orders)} orders → {len(summary)} daily summaries")
except pa.errors.SchemaError as err:
print(f"Validation failed:\n{err.failure_cases}")
# failure_cases is a DataFrame showing exactly which rows/columns failed
Custom Checks
# schemas/custom_checks.py — Reusable validation checks
import pandas as pd
import pandera.pandas as pa
import pandera.extensions as extensions
from pandera.typing import Series
@extensions.register_check_method(statistics=["threshold"])
def no_outliers_iqr(series: pd.Series, *, threshold: float = 1.5) -> pd.Series:
"""Flag values outside the IQR fence (1.5 = standard, 3.0 = extreme only)."""
q1 = series.quantile(0.25)
q3 = series.quantile(0.75)
iqr = q3 - q1
lower = q1 - threshold * iqr
upper = q3 + threshold * iqr
return (series >= lower) & (series <= upper)
# Usage in schema
class MetricsSchema(pa.DataFrameModel):
revenue: Series[float] = pa.Field(no_outliers_iqr={"threshold": 3.0})
latency_ms: Series[float] = pa.Field(no_outliers_iqr={"threshold": 1.5})
Polars Support
# schemas/polars_schema.py — Validate Polars DataFrames
import pandera.polars as pa
import polars as pl
class UserSchema(pa.DataFrameModel):
user_id: int = pa.Field(unique=True, gt=0)
email: str = pa.Field(str_matches=r"^[\w.-]+@[\w.-]+\.\w+quot;)
plan: str = pa.Field(isin=["free", "pro", "enterprise"])
mrr: float = pa.Field(ge=0)
# Validate a Polars DataFrame
df = pl.read_parquet("users.parquet")
validated = UserSchema.validate(df) # Returns validated Polars DataFrame
Integration with Pytest
# tests/test_data_quality.py — Data quality tests
import pandas as pd
import pytest
import pandera.pandas as pa
from schemas.orders import OrderSchema
def test_orders_schema_on_sample_data():
"""Verify the schema accepts known-good data."""
good_data = pd.DataFrame({
"order_id": ["ORD-00000001", "ORD-00000002"],
"customer_id": ["cust-1", "cust-2"],
"amount": [29.99, 149.00],
"status": ["completed", "pending"],
"currency": ["USD", "EUR"],
"created_at": pd.to_datetime(["2026-01-01", "2026-01-02"]),
"shipped_at": pd.to_datetime(["2026-01-03", pd.NaT]),
"items_count": [2, 5],
})
validated = OrderSchema.validate(good_data)
assert len(validated) == 2
def test_orders_schema_rejects_negative_amount():
"""Schema must reject orders with negative amounts."""
bad_data = pd.DataFrame({
"order_id": ["ORD-00000001"], "customer_id": ["cust-1"],
"amount": [-10.00], "status": ["completed"], "currency": ["USD"],
"created_at": pd.to_datetime(["2026-01-01"]),
"shipped_at": pd.to_datetime(["2026-01-02"]), "items_count": [1],
})
with pytest.raises(pa.errors.SchemaError):
OrderSchema.validate(bad_data)
Installation
pip install "pandera[pandas]" # pandas DataFrames (import pandera.pandas)
pip install "pandera[polars]" # Polars DataFrames (import pandera.polars)
pip install "pandera[io]" # to_yaml / from_yaml schema serialization
pip install "pandera[hypotheses]" # hypothesis checks (needs scipy)
pip install "pandera[strategies]" # data synthesis with hypothesis
The plain pip install pandera no longer pulls in pandas, so install the extra for the backend you use.
Examples
Example 1: Report every bad row in a CSV before loading it
User request: "Our nightly orders.csv sometimes has malformed IDs and negative amounts. Validate it and tell me every failing row, not just the first."
By default Pandera stops at the first failure. Pass lazy=True to collect all of them; they raise pa.errors.SchemaErrors (plural) with a failure_cases DataFrame.
import pandas as pd
import pandera.pandas as pa
from schemas.orders import OrderSchema
df = pd.read_csv("data/orders_2026_03.csv", parse_dates=["created_at", "shipped_at"])
try:
OrderSchema.validate(df, lazy=True)
except pa.errors.SchemaErrors as err:
print(err.failure_cases[["index", "column", "check", "failure_case"]])
Result: one row per violation, for example order_id | str_matches('^ORD-\\d{8}#x27;) | ORD-0002 and amount | greater_than_or_equal_to(0.01) | -1.0. Without lazy=True only the first violation raises pa.errors.SchemaError.
Example 2: Data contract for a Polars users table in CI
User request: "Add a check to our pipeline that fails if users.parquet has duplicate ids or an unknown plan."
import pandera.polars as pa
import polars as pl
class UserSchema(pa.DataFrameModel):
user_id: int = pa.Field(unique=True, gt=0)
plan: str = pa.Field(isin=["free", "pro", "enterprise"])
UserSchema.validate(pl.read_parquet("users.parquet"), lazy=True)
A valid frame is returned unchanged. A frame with user_id 1 twice and plan "x" raises SchemaErrors; failure_cases is a Polars DataFrame listing field_uniqueness failures for user_id and the isin failure for plan. Without lazy=True Polars raises SchemaError at the first failure.
Guidelines
- Schema at the boundary — Validate data at ingestion points (file loads, API responses, database queries); don't trust upstream
- Use DataFrameModel over raw SchemaModel — Class-based schemas give you type hints, IDE autocomplete, and cleaner code
- Strict mode — Enable
strict = True to reject unexpected columns; prevents schema drift
- Coercion for robustness — Enable
coerce = True to auto-convert types (string "123" → int 123) before validation
- Cross-column checks — Use
@dataframe_check for rules that span multiple columns (shipped_at > created_at)
- Test your schemas — Write pytest tests with known-good and known-bad data to verify schema behavior
- Descriptive error messages — Pandera's
failure_cases DataFrame shows exactly which rows and columns failed and why
- Schema evolution — When requirements change, update the schema first; let validation catch all affected data
- Pick the right import —
import pandera.pandas as pa for pandas, import pandera.polars as pa for Polars; the old top-level import pandera as pa still works for pandas, but the submodule is what the documentation uses
- Lazy validation for reports — use
lazy=True when you want all failures; leave it off for fail-fast pipelines
- Cost — validation runs on every call; for large frames validate at boundaries, or use
head, tail or sample arguments on validate for a cheaper spot check