Skip to content

Add spec for generic dataframe-agnostic schema API - #2401

Open
cosmicbboy wants to merge 3 commits into
mainfrom
nielsb/generic-dataframe-spec
Open

cosmicbboy wants to merge 3 commits into
mainfrom
nielsb/generic-dataframe-spec

Conversation

@cosmicbboy

@cosmicbboy cosmicbboy commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds specs/generic-dataframe.md, a design document for a dataframe-agnostic DataFrameSchema/DataFrameModel API built on the existing narwhals backend (pandera/backends/narwhals/) and engine (pandera/engines/narwhals_engine.py).

Today narwhals is wired in only as an alternate validation backend beneath the polars/ibis/pyspark schema classes (use_narwhals_backend). This spec defines the missing top half — a generic schema/model API surface, its typing story, and the roadmap by which it eventually replaces the per-library schema implementations.

Key design points:

  • The generic API lives in pandera/api/dataframe/ with public accessor pandera.dataframe; the abstract base classes currently in pandera/api/dataframe/ move to a new private package pandera/api/_base_dataframe/ (migration mechanics in §3.5: internal import rewrite, mypy-plugin fullname updates, isinstance-compatibility caveat). The existing narwhals backend is reused unmodified; the backend remains customizable through the existing BACKEND_REGISTRY mechanism.
  • Dynamic backend resolution: registration keyed on narwhals compatibility rather than an enumerated list of frame types, so any narwhals-supported library works with zero config.
  • Typing for mypy and pyright: input-type-preserving validate() (TFrame -> TFrame), mypy plugin fullname extensions, pyright-clean generics — following the approach from the static-typing work on nielsb/fix-series-typing (Improve DataFrame/Series static typing for mypy and Pyright #2398). Column-level inference is a mypy-plugin feature.
  • Custom checks against any DF type: portable NarwhalsData/nw.Expr checks by default, plus native-typed check signatures (e.g. pl.LazyFrame) with boundary conversion and singledispatch-style per-library overrides.
  • Three-tier extensibility for registering new frame types: automatic (narwhals-compatible), declarative register_frame_type(...), full backend override — plus a pandera.frame_types entry-point group.
  • Phased replacement roadmap (ibis → polars → pyspark → pandas), with the pandas Index/MultiIndex problem explicitly flagged and options laid out.
  • Generic NDArrayLike protocol + NDArray[Model] annotation for the xarray API, so xarray-like libraries can reuse DataArraySchema.

Open questions are listed at the end of the spec (base-class naming/module placement, error-report unification, entry-point security posture, strategies scope, narwhals version policy).

Spec only — no code changes.

🤖 Generated with Claude Code

Design document for a dataframe-agnostic DataFrameSchema/DataFrameModel
API built on the existing narwhals backend and engine: dynamic backend
resolution keyed on narwhals compatibility, mypy/pyright typing story,
native-typed custom checks, a three-tier frame-type registration model,
a phased roadmap for replacing the per-library schema implementations,
and a generic NDArrayLike protocol for the xarray API.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 6, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.19%. Comparing base (e870571) to head (7ae2459).
⚠️ Report is 17 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2401      +/-   ##
==========================================
- Coverage   90.92%   90.19%   -0.74%     
==========================================
  Files         192      193       +1     
  Lines       17116    18393    +1277     
==========================================
+ Hits        15563    16589    +1026     
- Misses       1553     1804     +251     
Flag Coverage Δ
unit-tests-base-Linux-py3.10 15.38% <ø> (-0.02%) ⬇️
unit-tests-base-Linux-py3.11 15.38% <ø> (-0.02%) ⬇️
unit-tests-base-Linux-py3.12 15.38% <ø> (-0.02%) ⬇️
unit-tests-base-Linux-py3.13 15.38% <ø> (-0.02%) ⬇️
unit-tests-base-Linux-py3.14 15.24% <ø> (-0.02%) ⬇️
unit-tests-base-Windows-py3.10 15.30% <ø> (-0.02%) ⬇️
unit-tests-base-Windows-py3.11 15.30% <ø> (-0.02%) ⬇️
unit-tests-base-Windows-py3.12 15.30% <ø> (-0.02%) ⬇️
unit-tests-base-Windows-py3.13 15.30% <ø> (-0.02%) ⬇️
unit-tests-base-Windows-py3.14 15.17% <ø> (-0.02%) ⬇️
unit-tests-base-macOS-py3.10 15.31% <ø> (-0.02%) ⬇️
unit-tests-base-macOS-py3.11 15.31% <ø> (-0.02%) ⬇️
unit-tests-base-macOS-py3.12 15.31% <ø> (-0.02%) ⬇️
unit-tests-base-macOS-py3.13 15.31% <ø> (-0.02%) ⬇️
unit-tests-base-macOS-py3.14 15.18% <ø> (-0.02%) ⬇️
unit-tests-dask-Linux-py3.10-pandas2.3.3-polars 24.23% <ø> (-0.02%) ⬇️
unit-tests-dask-Linux-py3.11-pandas2.3.3-polars 24.23% <ø> (-0.02%) ⬇️
unit-tests-dask-Linux-py3.12-pandas2.3.3-polars 24.23% <ø> (-0.02%) ⬇️
unit-tests-dask-Linux-py3.13-pandas2.3.3-polars 24.23% <ø> (-0.02%) ⬇️
unit-tests-dask-Linux-py3.14-pandas2.3.3-polars 24.14% <ø> (-0.02%) ⬇️
unit-tests-dask-Windows-py3.10-pandas2.3.3-polars 24.14% <ø> (-0.02%) ⬇️
unit-tests-dask-Windows-py3.11-pandas2.3.3-polars 24.14% <ø> (-0.02%) ⬇️
unit-tests-dask-Windows-py3.12-pandas2.3.3-polars 24.14% <ø> (-0.02%) ⬇️
unit-tests-dask-Windows-py3.13-pandas2.3.3-polars 24.14% <ø> (-0.02%) ⬇️
unit-tests-dask-Windows-py3.14-pandas2.3.3-polars 24.06% <ø> (-0.02%) ⬇️
unit-tests-fastapi-Linux-py3.10-pandas2.3.3-pydantic2.12.3 24.67% <ø> (-0.02%) ⬇️
unit-tests-fastapi-Linux-py3.11-pandas2.3.3-pydantic2.12.3 24.67% <ø> (-0.02%) ⬇️
unit-tests-fastapi-Linux-py3.11-pandas3.0.0-pydantic2.12.3 24.74% <ø> (-0.03%) ⬇️
unit-tests-fastapi-Linux-py3.13-pandas2.3.3-pydantic2.12.3 24.67% <ø> (-0.01%) ⬇️
unit-tests-fastapi-Linux-py3.13-pandas3.0.0-pydantic2.12.3 24.74% <ø> (-0.02%) ⬇️
unit-tests-fastapi-Windows-py3.11-pandas2.3.3-pydantic2.12.3 24.58% <ø> (+0.02%) ⬆️
unit-tests-geopandas-Linux-py3.11-pandas3.0.0-pydantic2.12.3 30.04% <ø> (-0.03%) ⬇️
unit-tests-geopandas-Linux-py3.12-pandas2.3.3-pydantic2.12.3 30.04% <ø> (-0.03%) ⬇️
unit-tests-geopandas-Linux-py3.12-pandas3.0.0-pydantic2.12.3 30.04% <ø> (-0.02%) ⬇️
unit-tests-geopandas-Windows-py3.10-pandas2.3.3-pydantic2.12.3 29.94% <ø> (-0.03%) ⬇️
unit-tests-hypotheses-Linux-py3.10-pandas2.3.3-pydantic2.12.3 22.48% <ø> (+<0.01%) ⬆️
unit-tests-hypotheses-Linux-py3.11-pandas3.0.0-pydantic2.12.3 22.51% <ø> (-0.01%) ⬇️
unit-tests-hypotheses-Linux-py3.12-pandas3.0.0-pydantic2.12.3 22.51% <ø> (-0.02%) ⬇️
unit-tests-hypotheses-Linux-py3.13-pandas2.3.3-pydantic2.12.3 22.48% <ø> (-0.01%) ⬇️
unit-tests-hypotheses-Linux-py3.13-pandas3.0.0-pydantic2.12.3 22.51% <ø> (-0.02%) ⬇️
unit-tests-hypotheses-Windows-py3.10-pandas2.3.3-pydantic2.12.3 22.40% <ø> (-0.02%) ⬇️
unit-tests-ibis-Linux-py3.10-pandas2.3.3-polars 28.85% <ø> (-0.01%) ⬇️
unit-tests-ibis-Linux-py3.11-pandas2.3.3-polars 28.85% <ø> (-0.01%) ⬇️
unit-tests-ibis-Linux-py3.12-pandas2.3.3-polars 28.85% <ø> (-0.01%) ⬇️
unit-tests-ibis-Linux-py3.13-pandas2.3.3-polars 28.85% <ø> (-0.01%) ⬇️
unit-tests-ibis-Linux-py3.14-pandas2.3.3-polars 28.66% <ø> (-0.01%) ⬇️
unit-tests-ibis-Windows-py3.10-pandas2.3.3-polars 28.78% <ø> (-0.01%) ⬇️
unit-tests-ibis-Windows-py3.11-pandas2.3.3-polars 28.78% <ø> (-0.01%) ⬇️
unit-tests-ibis-Windows-py3.12-pandas2.3.3-polars 28.78% <ø> (-0.01%) ⬇️
unit-tests-ibis-Windows-py3.13-pandas2.3.3-polars 28.78% <ø> (-0.01%) ⬇️
unit-tests-ibis-Windows-py3.14-pandas2.3.3-polars 28.59% <ø> (-0.01%) ⬇️
unit-tests-ibis-macOS-py3.11-pandas2.3.3-polars 28.79% <ø> (+<0.01%) ⬆️
unit-tests-io-Linux-py3.10-pandas2.3.3-pydantic2.12.3 38.09% <ø> (+0.03%) ⬆️
unit-tests-io-Linux-py3.13-pandas3.0.0-pydantic2.12.3 38.10% <ø> (?)
unit-tests-io-Linux-py3.14-pandas2.3.3-pydantic2.12.3 38.02% <ø> (-0.01%) ⬇️
unit-tests-modin-dask-Linux-py3.10-pandas2.3.3-polars 30.67% <ø> (-0.03%) ⬇️
unit-tests-modin-dask-Linux-py3.12-pandas2.3.3-polars 30.67% <ø> (-0.01%) ⬇️
unit-tests-modin-ray-Linux-py3.10-pandas2.3.3-polars 30.64% <ø> (-0.03%) ⬇️
unit-tests-modin-ray-Linux-py3.11-pandas2.3.3-polars 30.64% <ø> (-0.03%) ⬇️
unit-tests-modin-ray-Linux-py3.12-pandas2.3.3-polars 30.64% <ø> (-0.03%) ⬇️
unit-tests-mypy-Linux-py3.10-pandas2.3.3-pydantic2.12.3 23.43% <ø> (-0.02%) ⬇️
unit-tests-mypy-Linux-py3.11-pandas3.0.0-pydantic2.12.3 23.50% <ø> (?)
unit-tests-narwhals-Linux-py3.10 28.26% <ø> (-0.01%) ⬇️
unit-tests-narwhals-Linux-py3.11 28.26% <ø> (-0.01%) ⬇️
unit-tests-narwhals-Linux-py3.12 28.26% <ø> (-0.01%) ⬇️
unit-tests-narwhals-Linux-py3.13 28.26% <ø> (-0.01%) ⬇️
unit-tests-narwhals-Linux-py3.14 28.04% <ø> (-0.01%) ⬇️
unit-tests-narwhals-backend-ibis-py3.10 31.80% <ø> (-0.01%) ⬇️
unit-tests-narwhals-backend-ibis-py3.11 31.69% <ø> (-0.01%) ⬇️
unit-tests-narwhals-backend-ibis-py3.12 31.69% <ø> (-0.01%) ⬇️
unit-tests-narwhals-backend-ibis-py3.13 31.69% <ø> (-0.01%) ⬇️
unit-tests-narwhals-backend-ibis-py3.14 31.48% <ø> (-0.01%) ⬇️
unit-tests-narwhals-backend-polars-py3.10 34.48% <ø> (+0.04%) ⬆️
unit-tests-narwhals-backend-polars-py3.11 34.47% <ø> (+0.04%) ⬆️
unit-tests-narwhals-backend-polars-py3.12 34.47% <ø> (+0.04%) ⬆️
unit-tests-narwhals-backend-polars-py3.13 34.47% <ø> (+0.04%) ⬆️
unit-tests-narwhals-backend-polars-py3.14 34.33% <ø> (+0.04%) ⬆️
unit-tests-narwhals-backend-pyspark-py3.10 43.46% <ø> (?)
unit-tests-narwhals-backend-pyspark-py3.11 43.49% <ø> (?)
unit-tests-pandas-Linux-py3.10-pandas2.3.3-pydantic1.10.11 42.76% <ø> (-0.01%) ⬇️
unit-tests-pandas-Linux-py3.10-pandas2.3.3-pydantic2.12.3 42.91% <ø> (-0.01%) ⬇️
unit-tests-pandas-Linux-py3.11-pandas2.3.3-pydantic1.10.11 42.76% <ø> (-0.01%) ⬇️
unit-tests-pandas-Linux-py3.11-pandas2.3.3-pydantic2.12.3 42.91% <ø> (-0.01%) ⬇️
unit-tests-pandas-Linux-py3.11-pandas3.0.0-pydantic1.10.11 42.77% <ø> (-0.01%) ⬇️
unit-tests-pandas-Linux-py3.11-pandas3.0.0-pydantic2.12.3 42.93% <ø> (-0.01%) ⬇️
unit-tests-pandas-Linux-py3.12-pandas2.3.3-pydantic1.10.11 42.75% <ø> (-0.01%) ⬇️
unit-tests-pandas-Linux-py3.12-pandas2.3.3-pydantic2.12.3 42.90% <ø> (-0.01%) ⬇️
unit-tests-pandas-Linux-py3.12-pandas3.0.0-pydantic1.10.11 42.76% <ø> (-0.01%) ⬇️
unit-tests-pandas-Linux-py3.12-pandas3.0.0-pydantic2.12.3 42.91% <ø> (-0.01%) ⬇️
unit-tests-pandas-Linux-py3.13-pandas2.3.3-pydantic1.10.11 42.75% <ø> (-0.01%) ⬇️
unit-tests-pandas-Linux-py3.13-pandas2.3.3-pydantic2.12.3 42.90% <ø> (-0.01%) ⬇️
unit-tests-pandas-Linux-py3.13-pandas3.0.0-pydantic1.10.11 42.76% <ø> (-0.01%) ⬇️
unit-tests-pandas-Linux-py3.13-pandas3.0.0-pydantic2.12.3 42.91% <ø> (-0.01%) ⬇️
unit-tests-pandas-Linux-py3.14-pandas2.3.3-pydantic2.12.3 42.88% <ø> (-0.01%) ⬇️
unit-tests-pandas-Linux-py3.14-pandas3.0.0-pydantic2.12.3 42.90% <ø> (-0.01%) ⬇️
unit-tests-pandas-Windows-py3.10-pandas2.3.3-pydantic1.10.11 42.67% <ø> (-0.01%) ⬇️
unit-tests-pandas-Windows-py3.10-pandas2.3.3-pydantic2.12.3 42.83% <ø> (-0.01%) ⬇️
unit-tests-pandas-Windows-py3.11-pandas2.3.3-pydantic1.10.11 42.67% <ø> (-0.01%) ⬇️
unit-tests-pandas-Windows-py3.11-pandas2.3.3-pydantic2.12.3 42.83% <ø> (-0.01%) ⬇️
unit-tests-pandas-Windows-py3.11-pandas3.0.0-pydantic1.10.11 42.68% <ø> (-0.01%) ⬇️
unit-tests-pandas-Windows-py3.11-pandas3.0.0-pydantic2.12.3 42.84% <ø> (-0.01%) ⬇️
unit-tests-pandas-Windows-py3.12-pandas2.3.3-pydantic1.10.11 42.66% <ø> (-0.01%) ⬇️
unit-tests-pandas-Windows-py3.12-pandas3.0.0-pydantic1.10.11 42.67% <ø> (-0.01%) ⬇️
unit-tests-pandas-Windows-py3.12-pandas3.0.0-pydantic2.12.3 42.83% <ø> (-0.01%) ⬇️
unit-tests-pandas-Windows-py3.13-pandas2.3.3-pydantic1.10.11 42.66% <ø> (-0.01%) ⬇️
unit-tests-pandas-Windows-py3.13-pandas2.3.3-pydantic2.12.3 42.81% <ø> (-0.01%) ⬇️
unit-tests-pandas-Windows-py3.13-pandas3.0.0-pydantic1.10.11 42.67% <ø> (-0.01%) ⬇️
unit-tests-pandas-Windows-py3.13-pandas3.0.0-pydantic2.12.3 42.83% <ø> (-0.01%) ⬇️
unit-tests-pandas-Windows-py3.14-pandas2.3.3-pydantic2.12.3 42.80% <ø> (-0.01%) ⬇️
unit-tests-pandas-Windows-py3.14-pandas3.0.0-pydantic2.12.3 42.81% <ø> (-0.01%) ⬇️
unit-tests-pandas-macOS-py3.10-pandas2.3.3-pydantic1.10.11 42.70% <ø> (-0.01%) ⬇️
unit-tests-pandas-macOS-py3.10-pandas2.3.3-pydantic2.12.3 42.85% <ø> (-0.01%) ⬇️
unit-tests-pandas-macOS-py3.11-pandas2.3.3-pydantic1.10.11 42.70% <ø> (-0.01%) ⬇️
unit-tests-pandas-macOS-py3.11-pandas2.3.3-pydantic2.12.3 42.85% <ø> (-0.01%) ⬇️
unit-tests-pandas-macOS-py3.11-pandas3.0.0-pydantic1.10.11 42.71% <ø> (-0.01%) ⬇️
unit-tests-pandas-macOS-py3.11-pandas3.0.0-pydantic2.12.3 42.86% <ø> (-0.01%) ⬇️
unit-tests-pandas-macOS-py3.12-pandas2.3.3-pydantic1.10.11 42.68% <ø> (-0.01%) ⬇️
unit-tests-pandas-macOS-py3.12-pandas2.3.3-pydantic2.12.3 42.84% <ø> (-0.01%) ⬇️
unit-tests-pandas-macOS-py3.12-pandas3.0.0-pydantic1.10.11 42.70% <ø> (-0.01%) ⬇️
unit-tests-pandas-macOS-py3.12-pandas3.0.0-pydantic2.12.3 42.85% <ø> (-0.01%) ⬇️
unit-tests-pandas-macOS-py3.13-pandas2.3.3-pydantic1.10.11 42.68% <ø> (-0.01%) ⬇️
unit-tests-pandas-macOS-py3.13-pandas2.3.3-pydantic2.12.3 42.84% <ø> (-0.01%) ⬇️
unit-tests-pandas-macOS-py3.13-pandas3.0.0-pydantic1.10.11 42.70% <ø> (-0.01%) ⬇️
unit-tests-pandas-macOS-py3.13-pandas3.0.0-pydantic2.12.3 42.85% <ø> (-0.01%) ⬇️
unit-tests-pandas-macOS-py3.14-pandas2.3.3-pydantic2.12.3 42.82% <ø> (-0.01%) ⬇️
unit-tests-pandas-macOS-py3.14-pandas3.0.0-pydantic2.12.3 42.83% <ø> (-0.01%) ⬇️
unit-tests-polars-Linux-py3.10-pandas2.3.3-polars1.33.1 31.08% <ø> (+0.04%) ⬆️
unit-tests-polars-Linux-py3.11-pandas2.3.3-polars1.33.1 31.07% <ø> (+0.04%) ⬆️
unit-tests-polars-Linux-py3.12-pandas2.3.3-polars1.33.1 31.07% <ø> (+0.04%) ⬆️
unit-tests-polars-Linux-py3.13-pandas2.3.3-polars1.33.1 31.07% <ø> (+0.04%) ⬆️
unit-tests-polars-Linux-py3.14-pandas2.3.3-polars1.33.1 30.94% <ø> (+0.04%) ⬆️
unit-tests-polars-Windows-py3.10-pandas2.3.3-polars1.33.1 31.01% <ø> (+0.04%) ⬆️
unit-tests-polars-Windows-py3.11-pandas2.3.3-polars1.33.1 30.99% <ø> (+0.04%) ⬆️
unit-tests-polars-Windows-py3.12-pandas2.3.3-polars1.33.1 30.99% <ø> (+0.04%) ⬆️
unit-tests-polars-Windows-py3.13-pandas2.3.3-polars1.33.1 30.99% <ø> (+0.04%) ⬆️
unit-tests-polars-Windows-py3.14-pandas2.3.3-polars1.33.1 30.87% <ø> (+0.04%) ⬆️
unit-tests-polars-macOS-py3.10-pandas2.3.3-polars1.33.1 31.02% <ø> (+0.04%) ⬆️
unit-tests-polars-macOS-py3.11-pandas2.3.3-polars1.33.1 31.01% <ø> (+0.04%) ⬆️
unit-tests-strategies-Linux-py3.13-pandas3.0.0-pydantic2.12.3 34.14% <ø> (-0.05%) ⬇️
unit-tests-strategies-Linux-py3.14-pandas3.0.0-pydantic2.12.3 34.09% <ø> (?)
unit-tests-xarray-Linux-py3.10-pandas2.3.3-polars 31.73% <ø> (-0.03%) ⬇️
unit-tests-xarray-Linux-py3.11-pandas2.3.3-polars 31.73% <ø> (-0.03%) ⬇️
unit-tests-xarray-Linux-py3.12-pandas2.3.3-polars 31.73% <ø> (-0.03%) ⬇️
unit-tests-xarray-Linux-py3.13-pandas2.3.3-polars 31.73% <ø> (-0.03%) ⬇️
unit-tests-xarray-Windows-py3.10-pandas2.3.3-polars 31.66% <ø> (-0.03%) ⬇️
unit-tests-xarray-Windows-py3.11-pandas2.3.3-polars 31.66% <ø> (-0.03%) ⬇️
unit-tests-xarray-Windows-py3.12-pandas2.3.3-polars 31.66% <ø> (-0.03%) ⬇️
unit-tests-xarray-Windows-py3.13-pandas2.3.3-polars 31.66% <ø> (-0.03%) ⬇️
unit-tests-xarray-Windows-py3.14-pandas2.3.3-polars 31.58% <ø> (-0.03%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

The generate-and-paste pyright workflow was removed from the
fix-series-typing branch as poor devex; the spec now scopes column-level
inference to the mypy plugin, with pyright relying on plugin-free
generics only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cosmicbboy

Copy link
Copy Markdown
Collaborator Author

Updated the typing section (§5): removed the pandera/typing/codegen.py approach for pyright column-level inference — the generate-and-paste workflow was rejected as poor devex and removed from nielsb/fix-series-typing (see #2398). Column-level inference is now scoped as a mypy-plugin-only feature; pyright/Pylance gets schema-level types via plugin-free generics.

The generic (narwhals-default) DataFrameSchema/DataFrameModel API takes
over pandera/api/dataframe/; the abstract base classes currently there
move to a new private package, pandera/api/_base_dataframe/. Public
accessor becomes pandera.dataframe instead of pandera.narwhals.

Adds §3.5 documenting the migration mechanics of the takeover: internal
import rewrite (~20 modules), mypy-plugin fullname updates, and the
isinstance-compatibility caveat for external imports of the old paths
until Phase 3 re-parents the per-library classes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant