Skip to content

feat(narwhals): support the pandas schema API in the narwhals backend - #2417

Merged
cosmicBboy merged 5 commits into
mainfrom
narwhals-pandas-backend
Jul 31, 2026
Merged

feat(narwhals): support the pandas schema API in the narwhals backend#2417
cosmicBboy merged 5 commits into
mainfrom
narwhals-pandas-backend

Conversation

@cosmicBboy

@cosmicBboy cosmicBboy commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

Routes pd.DataFrame validation through the shared Narwhals backend when it is enabled (PANDERA_USE_NARWHALS_BACKEND=True or pandera.set_config(use_narwhals_backend=True)), following the registration pattern established for polars, ibis, and pyspark. The flag remains off by default — native behavior is unchanged.

Because pandas frames are eager, the Narwhals pandas path now reaches near feature-parity with the native pandas backend. Almost every step is handled Narwhals-native; only two things are delegated to the native pandas backend:

  • Index / MultiIndex validation + coercion — Narwhals has no index concept, but it preserves the pandas index through its operations, so the index component is validated/coerced by delegating to the native Index/MultiIndex backends and the index survives to the output.
  • Hypothesis checks — these rely on scipy statistical tests with no Narwhals-expression equivalent.

Scope: only pd.DataFrame validation via DataFrameSchema / DataFrameModel is rerouted. SeriesSchema and the other pandas-like frame types (dask, modin, geopandas, pyspark.pandas) always stay on the native pandas backends.

Changes

Registration

  • register_pandas_backends() gains a use_narwhals_backend flag that force-overrides only the pd.DataFrame registry entries for DataFrameSchema (including the deprecated root-level class), Column, and narwhals-frame Check dispatch. The (Check, pd.DataFrame) entry intentionally stays native so direct Check calls on native frames are unchanged.
  • Because the pandas register function is fqn-parameterized (unlike polars/ibis/pyspark), a new re_register_pandas_backends() replays every registered fqn with the new flag value; set_config() re-registration and clear_narwhals_compatible_backend_registry() now cover pandas.
  • pandera.pandas Column gains the selector property the narwhals backend depends on.

Narwhals backend adaptations (all gated to pandas-like frames; polars/ibis/pyspark paths unchanged)

  • Native dtype semantics for checks: check_dtype compares native pandas dtypes through the pandas engine, so Column(str) accepts object columns, nullable Int64, categoricals, and tz-aware datetimes behave as under the native backend.
  • pandas-style checks keep working: native=True column checks receive the pd.Series, dataframe-level checks the pd.DataFrame; boolean Series/DataFrame/ndarray and numpy-scalar outputs are normalized. native=False passes a narwhals column expression as with the other backends.
  • coerce=True (column- and schema-level): hybrid strategy — plain numpy dtypes cast via nw.cast (Narwhals-native); pandas extension dtypes (nullable Int64, Categorical, tz-aware datetime, string) and any cast that raises fall back to the pandas dtype engine so native semantics are preserved. Failures report DATATYPE_COERCION with the offending values. Column-level coerce is no longer a no-op warning on pandas.
  • add_missing_columns, set_default, custom parsers: applied Narwhals-native. Parsers run on the native frame pre-wrap (arbitrary user code); missing/default columns are built from Narwhals expressions (nw.lit/fill_null/cast) and follow the same hybrid dtype rule as coercion (a missing nullable Int64 is added as Int64/<NA>, not object); missing columns are inserted at their schema position.
  • groupby column-check-groups: run Narwhals-native via NarwhalsCheckBackend.apply_groupby (builds the pandas group dict, reduces per-group output to a single pass/fail). groups= filtering supported.
  • unique_column_names: detected on the native frame pre-wrap, since Narwhals rejects duplicate column labels at wrap time.
  • ignore_na correctness fix for NaN: pandas comparisons on NaN yield False rather than propagating null, so ignore_na now also ORs in the input column's nullness — in check evaluation, deferred failure-case reconstruction, and drop_invalid_rows.
  • Failure cases are pandas frames: the narwhals-native builder is used for pandas-backed failure cases (the eager builder is a polars-specific Arrow path that leaked __index_level_0__); _concat_failure_cases gains a pandas-like branch, and the all-scalar case (e.g. dtype-only errors) resolves the frame type from the schema class. Scalar failure cases no longer hard-require polars.

Docs

  • narwhals_backend.md: rewritten "pandas: differences from the native backend" section describing the parity and the two delegated gaps (Index/MultiIndex, Hypothesis); known-gaps list scoped to non-pandas backends where appropriate. configuration.md, supported_libraries.md, reference/narwhals.rst, and config.py docstrings updated.

Tests

  • tests/narwhals/backends/ parity suite runs against pandas as a third backend (new PandasBackend fixture + registered pandas marker).
  • tests/narwhals/test_pandas_narwhals_register.py: activation/deactivation, native fallback, idempotence, fqn replay, set_config runtime toggling.
  • tests/narwhals/test_pandas_e2e.py: native dtype semantics, pandas-style/aggregate/element-wise/expression checks, NaN ignore_na incl. drop_invalid_rows, pandas failure-case types with no index leakage, regex columns, DataFrameModel, ordered/strict — plus the new parity coverage: Index/MultiIndex validation + coercion, hybrid coercion fidelity (numpy via nw.cast, extension dtypes via pandas engine, failure reporting), custom parsers (chained/element-wise), add_missing_columns (ordering, extension-dtype fidelity, required-default errors), set_default, groupby checks (dict output, groups= filter), Hypothesis checks, and unique_column_names.
  • pandas is intentionally not added to the tests_narwhals_backend nox matrix — the full tests/pandas/ suite exercises features the narwhals backend does not support (rationale documented in the noxfile). The new suites run in the existing unit-tests-narwhals CI job.

Verification

  • Flag off (no behavior change): native tests/pandas + tests/base + tests/polars + tests/ibis unchanged.
  • Flag on: tests/narwhals/test_pandas_e2e.py → 72 passed; full tests/narwhals suite shows no new failures relative to main (the same pre-existing ibis/pyspark local-env failures reproduce identically on the unmodified branch point).
  • pre-commit (ruff, mypy, etc.) passes.

🤖 Generated with Claude Code

@codecov

codecov Bot commented Jul 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.27731% with 32 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.41%. Comparing base (9d86aed) to head (8039f12).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
pandera/backends/narwhals/container.py 94.06% 13 Missing ⚠️
pandera/backends/narwhals/checks.py 93.54% 6 Missing ⚠️
pandera/backends/narwhals/base.py 92.75% 5 Missing ⚠️
pandera/backends/narwhals/register.py 84.61% 4 Missing ⚠️
pandera/backends/narwhals/components.py 86.95% 3 Missing ⚠️
pandera/backends/pandas/register.py 96.29% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2417      +/-   ##
==========================================
+ Coverage   91.21%   91.41%   +0.20%     
==========================================
  Files         193      193              
  Lines       17135    17673     +538     
==========================================
+ Hits        15629    16156     +527     
- Misses       1506     1517      +11     
Flag Coverage Δ
unit-tests-base-Linux-py3.10 16.49% <2.55%> (+1.11%) ⬆️
unit-tests-base-Linux-py3.11 16.49% <2.55%> (+1.11%) ⬆️
unit-tests-base-Linux-py3.12 16.49% <2.55%> (+1.11%) ⬆️
unit-tests-base-Linux-py3.13 16.49% <2.55%> (+1.11%) ⬆️
unit-tests-base-Linux-py3.14 16.36% <2.55%> (+1.12%) ⬆️
unit-tests-base-Windows-py3.10 16.42% <2.55%> (+1.11%) ⬆️
unit-tests-base-Windows-py3.11 16.42% <2.55%> (+1.11%) ⬆️
unit-tests-base-Windows-py3.12 16.42% <2.55%> (+1.11%) ⬆️
unit-tests-base-Windows-py3.13 16.42% <2.55%> (+1.11%) ⬆️
unit-tests-base-Windows-py3.14 16.29% <2.55%> (+1.12%) ⬆️
unit-tests-base-macOS-py3.10 16.43% <2.55%> (+1.11%) ⬆️
unit-tests-base-macOS-py3.11 16.43% <2.55%> (+1.11%) ⬆️
unit-tests-base-macOS-py3.12 16.43% <2.55%> (+1.11%) ⬆️
unit-tests-base-macOS-py3.13 16.43% <2.55%> (+1.11%) ⬆️
unit-tests-base-macOS-py3.14 16.30% <2.55%> (+1.12%) ⬆️
unit-tests-dask-Linux-py3.10-pandas2.3.3-polars 23.66% <2.97%> (-0.57%) ⬇️
unit-tests-dask-Linux-py3.11-pandas2.3.3-polars 23.66% <2.97%> (-0.57%) ⬇️
unit-tests-dask-Linux-py3.12-pandas2.3.3-polars 23.66% <2.97%> (-0.57%) ⬇️
unit-tests-dask-Linux-py3.13-pandas2.3.3-polars 23.66% <2.97%> (-0.57%) ⬇️
unit-tests-dask-Linux-py3.14-pandas2.3.3-polars 23.58% <2.97%> (-0.57%) ⬇️
unit-tests-dask-Windows-py3.10-pandas2.3.3-polars 23.58% <2.97%> (-0.57%) ⬇️
unit-tests-dask-Windows-py3.11-pandas2.3.3-polars 23.58% <2.97%> (-0.57%) ⬇️
unit-tests-dask-Windows-py3.12-pandas2.3.3-polars 23.58% <2.97%> (-0.57%) ⬇️
unit-tests-dask-Windows-py3.13-pandas2.3.3-polars 23.58% <2.97%> (-0.57%) ⬇️
unit-tests-dask-Windows-py3.14-pandas2.3.3-polars 23.49% <2.97%> (-0.57%) ⬇️
unit-tests-dask-macOS-py3.10-pandas2.3.3-polars 23.60% <2.97%> (-0.57%) ⬇️
unit-tests-dask-macOS-py3.11-pandas2.3.3-polars 23.60% <2.97%> (-0.60%) ⬇️
unit-tests-fastapi-Linux-py3.11-pandas2.3.3-pydantic2.12.3 24.09% <2.97%> (-0.60%) ⬇️
unit-tests-geopandas-Linux-py3.10-pandas2.3.3-pydantic2.12.3 29.32% <3.19%> (-0.77%) ⬇️
unit-tests-geopandas-Linux-py3.13-pandas2.3.3-pydantic2.12.3 29.32% <3.19%> (-0.73%) ⬇️
unit-tests-geopandas-Linux-py3.13-pandas3.0.0-pydantic2.12.3 29.32% <3.19%> (-0.77%) ⬇️
unit-tests-geopandas-Windows-py3.10-pandas2.3.3-pydantic2.12.3 29.22% <3.19%> (-0.76%) ⬇️
unit-tests-hypotheses-Linux-py3.11-pandas2.3.3-pydantic2.12.3 21.95% <2.97%> (-0.53%) ⬇️
unit-tests-hypotheses-Linux-py3.11-pandas3.0.0-pydantic2.12.3 21.99% <2.97%> (-0.53%) ⬇️
unit-tests-hypotheses-Linux-py3.12-pandas3.0.0-pydantic2.12.3 21.99% <2.97%> (-0.55%) ⬇️
unit-tests-hypotheses-Linux-py3.14-pandas3.0.0-pydantic2.12.3 21.89% <2.97%> (-0.53%) ⬇️
unit-tests-hypotheses-Windows-py3.10-pandas2.3.3-pydantic2.12.3 21.88% <2.97%> (-0.52%) ⬇️
unit-tests-ibis-Linux-py3.10-pandas2.3.3-polars 28.11% <0.63%> (-0.75%) ⬇️
unit-tests-ibis-Linux-py3.11-pandas2.3.3-polars 28.11% <0.63%> (-0.75%) ⬇️
unit-tests-ibis-Linux-py3.12-pandas2.3.3-polars 28.11% <0.63%> (-0.75%) ⬇️
unit-tests-ibis-Linux-py3.13-pandas2.3.3-polars 28.11% <0.63%> (-0.75%) ⬇️
unit-tests-ibis-Linux-py3.14-pandas2.3.3-polars 27.92% <0.63%> (-0.75%) ⬇️
unit-tests-ibis-Windows-py3.10-pandas2.3.3-polars 28.03% <0.63%> (-0.75%) ⬇️
unit-tests-ibis-Windows-py3.11-pandas2.3.3-polars 28.03% <0.63%> (-0.75%) ⬇️
unit-tests-ibis-Windows-py3.12-pandas2.3.3-polars 28.03% <0.63%> (-0.75%) ⬇️
unit-tests-ibis-Windows-py3.13-pandas2.3.3-polars 28.03% <0.63%> (-0.75%) ⬇️
unit-tests-ibis-Windows-py3.14-pandas2.3.3-polars 27.85% <0.63%> (-0.75%) ⬇️
unit-tests-ibis-macOS-py3.10-pandas2.3.3-polars 28.05% <0.63%> (-0.74%) ⬇️
unit-tests-ibis-macOS-py3.11-pandas2.3.3-polars 28.05% <0.63%> (-0.74%) ⬇️
unit-tests-io-Linux-py3.10-pandas2.3.3-pydantic2.12.3 37.15% <2.97%> (-0.94%) ⬇️
unit-tests-io-Linux-py3.11-pandas3.0.0-pydantic2.12.3 37.16% <2.97%> (-0.94%) ⬇️
unit-tests-io-Linux-py3.13-pandas3.0.0-pydantic2.12.3 37.16% <2.97%> (?)
unit-tests-modin-dask-Linux-py3.10-pandas2.3.3-polars 29.93% <3.19%> (-0.78%) ⬇️
unit-tests-modin-dask-Linux-py3.11-pandas2.3.3-polars 29.93% <3.19%> (-0.78%) ⬇️
unit-tests-modin-dask-Linux-py3.12-pandas2.3.3-polars 29.93% <3.19%> (-0.74%) ⬇️
unit-tests-modin-dask-macOS-py3.10-pandas2.3.3-polars 29.87% <3.19%> (-0.78%) ⬇️
unit-tests-modin-ray-Linux-py3.10-pandas2.3.3-polars 29.91% <3.19%> (-0.74%) ⬇️
unit-tests-modin-ray-Linux-py3.11-pandas2.3.3-polars 29.91% <3.19%> (-0.74%) ⬇️
unit-tests-modin-ray-Linux-py3.12-pandas2.3.3-polars 29.91% <3.19%> (-0.74%) ⬇️
unit-tests-modin-ray-macOS-py3.10-pandas2.3.3-polars 29.84% <3.19%> (-0.78%) ⬇️
unit-tests-modin-ray-macOS-py3.11-pandas2.3.3-polars 29.84% <3.19%> (-0.78%) ⬇️
unit-tests-narwhals-Linux-py3.10 40.39% <91.70%> (+12.12%) ⬆️
unit-tests-narwhals-Linux-py3.11 40.38% <91.70%> (+12.12%) ⬆️
unit-tests-narwhals-Linux-py3.12 40.38% <91.70%> (+12.12%) ⬆️
unit-tests-narwhals-Linux-py3.13 40.38% <91.70%> (+12.12%) ⬆️
unit-tests-narwhals-Linux-py3.14 40.22% <91.70%> (+12.18%) ⬆️
unit-tests-narwhals-backend-ibis-py3.10 31.61% <27.44%> (-0.20%) ⬇️
unit-tests-narwhals-backend-ibis-py3.11 31.50% <27.44%> (-0.19%) ⬇️
unit-tests-narwhals-backend-ibis-py3.12 31.50% <27.44%> (-0.19%) ⬇️
unit-tests-narwhals-backend-ibis-py3.13 31.50% <27.44%> (-0.19%) ⬇️
unit-tests-narwhals-backend-ibis-py3.14 31.30% <27.44%> (-0.19%) ⬇️
unit-tests-narwhals-backend-polars-py3.10 36.09% <44.89%> (+1.60%) ⬆️
unit-tests-narwhals-backend-polars-py3.11 36.08% <44.89%> (+1.60%) ⬆️
unit-tests-narwhals-backend-polars-py3.12 36.08% <44.89%> (+1.60%) ⬆️
unit-tests-narwhals-backend-polars-py3.13 36.08% <44.89%> (+1.60%) ⬆️
unit-tests-narwhals-backend-polars-py3.14 35.94% <44.89%> (+1.61%) ⬆️
unit-tests-narwhals-backend-pyspark-py3.10 43.26% <37.23%> (-0.21%) ⬇️
unit-tests-narwhals-backend-pyspark-py3.11 43.29% <37.23%> (-0.21%) ⬇️
unit-tests-pandas-Linux-py3.10-pandas2.3.3-pydantic1.10.11 41.70% <3.40%> (-1.06%) ⬇️
unit-tests-pandas-Linux-py3.10-pandas2.3.3-pydantic2.12.3 41.85% <3.40%> (-1.07%) ⬇️
unit-tests-pandas-Linux-py3.11-pandas2.3.3-pydantic1.10.11 41.70% <3.40%> (-1.06%) ⬇️
unit-tests-pandas-Linux-py3.11-pandas2.3.3-pydantic2.12.3 41.85% <3.40%> (-1.07%) ⬇️
unit-tests-pandas-Linux-py3.11-pandas3.0.0-pydantic1.10.11 41.71% <3.40%> (-1.06%) ⬇️
unit-tests-pandas-Linux-py3.11-pandas3.0.0-pydantic2.12.3 41.86% <3.40%> (-1.07%) ⬇️
unit-tests-pandas-Linux-py3.12-pandas2.3.3-pydantic1.10.11 41.69% <3.40%> (-1.06%) ⬇️
unit-tests-pandas-Linux-py3.12-pandas2.3.3-pydantic2.12.3 41.84% <3.40%> (-1.07%) ⬇️
unit-tests-pandas-Linux-py3.12-pandas3.0.0-pydantic1.10.11 41.70% <3.40%> (-1.06%) ⬇️
unit-tests-pandas-Linux-py3.12-pandas3.0.0-pydantic2.12.3 41.85% <3.40%> (-1.07%) ⬇️
unit-tests-pandas-Linux-py3.13-pandas2.3.3-pydantic1.10.11 41.69% <3.40%> (-1.06%) ⬇️
unit-tests-pandas-Linux-py3.13-pandas2.3.3-pydantic2.12.3 41.84% <3.40%> (-1.07%) ⬇️
unit-tests-pandas-Linux-py3.13-pandas3.0.0-pydantic1.10.11 41.70% <3.40%> (-1.06%) ⬇️
unit-tests-pandas-Linux-py3.13-pandas3.0.0-pydantic2.12.3 41.85% <3.40%> (-1.07%) ⬇️
unit-tests-pandas-Linux-py3.14-pandas2.3.3-pydantic2.12.3 41.82% <3.40%> (-1.07%) ⬇️
unit-tests-pandas-Linux-py3.14-pandas3.0.0-pydantic2.12.3 41.83% <3.40%> (-1.07%) ⬇️
unit-tests-pandas-Windows-py3.10-pandas2.3.3-pydantic1.10.11 41.61% <3.40%> (-1.06%) ⬇️
unit-tests-pandas-Windows-py3.10-pandas2.3.3-pydantic2.12.3 41.77% <3.40%> (-1.06%) ⬇️
unit-tests-pandas-Windows-py3.11-pandas2.3.3-pydantic1.10.11 41.61% <3.40%> (-1.06%) ⬇️
unit-tests-pandas-Windows-py3.11-pandas2.3.3-pydantic2.12.3 41.77% <3.40%> (-1.06%) ⬇️
unit-tests-pandas-Windows-py3.11-pandas3.0.0-pydantic1.10.11 41.63% <3.40%> (-1.06%) ⬇️
unit-tests-pandas-Windows-py3.11-pandas3.0.0-pydantic2.12.3 41.78% <3.40%> (-1.07%) ⬇️
unit-tests-pandas-Windows-py3.12-pandas2.3.3-pydantic1.10.11 41.60% <3.40%> (-1.06%) ⬇️
unit-tests-pandas-Windows-py3.12-pandas2.3.3-pydantic2.12.3 41.75% <3.40%> (-1.06%) ⬇️
unit-tests-pandas-Windows-py3.12-pandas3.0.0-pydantic1.10.11 41.61% <3.40%> (-1.06%) ⬇️
unit-tests-pandas-Windows-py3.12-pandas3.0.0-pydantic2.12.3 41.77% <3.40%> (-1.06%) ⬇️
unit-tests-pandas-Windows-py3.13-pandas2.3.3-pydantic1.10.11 41.60% <3.40%> (-1.06%) ⬇️
unit-tests-pandas-Windows-py3.13-pandas2.3.3-pydantic2.12.3 41.75% <3.40%> (-1.06%) ⬇️
unit-tests-pandas-Windows-py3.13-pandas3.0.0-pydantic1.10.11 41.61% <3.40%> (-1.06%) ⬇️
unit-tests-pandas-Windows-py3.13-pandas3.0.0-pydantic2.12.3 41.77% <3.40%> (-1.06%) ⬇️
unit-tests-pandas-Windows-py3.14-pandas2.3.3-pydantic2.12.3 41.73% <3.40%> (-1.07%) ⬇️
unit-tests-pandas-Windows-py3.14-pandas3.0.0-pydantic2.12.3 41.75% <3.40%> (-1.07%) ⬇️
unit-tests-pandas-macOS-py3.10-pandas2.3.3-pydantic1.10.11 41.64% <3.40%> (-1.06%) ⬇️
unit-tests-pandas-macOS-py3.10-pandas2.3.3-pydantic2.12.3 41.79% <3.40%> (-1.07%) ⬇️
unit-tests-pandas-macOS-py3.11-pandas2.3.3-pydantic1.10.11 41.64% <3.40%> (-1.06%) ⬇️
unit-tests-pandas-macOS-py3.11-pandas2.3.3-pydantic2.12.3 41.79% <3.40%> (-1.07%) ⬇️
unit-tests-pandas-macOS-py3.11-pandas3.0.0-pydantic1.10.11 41.65% <3.40%> (-1.06%) ⬇️
unit-tests-pandas-macOS-py3.11-pandas3.0.0-pydantic2.12.3 41.80% <3.40%> (-1.07%) ⬇️
unit-tests-pandas-macOS-py3.12-pandas2.3.3-pydantic1.10.11 41.63% <3.40%> (-1.06%) ⬇️
unit-tests-pandas-macOS-py3.12-pandas2.3.3-pydantic2.12.3 41.78% <3.40%> (-1.07%) ⬇️
unit-tests-pandas-macOS-py3.12-pandas3.0.0-pydantic1.10.11 41.64% <3.40%> (-1.06%) ⬇️
unit-tests-pandas-macOS-py3.12-pandas3.0.0-pydantic2.12.3 41.79% <3.40%> (-1.07%) ⬇️
unit-tests-pandas-macOS-py3.13-pandas2.3.3-pydantic1.10.11 41.63% <3.40%> (-1.06%) ⬇️
unit-tests-pandas-macOS-py3.13-pandas2.3.3-pydantic2.12.3 41.78% <3.40%> (-1.07%) ⬇️
unit-tests-pandas-macOS-py3.13-pandas3.0.0-pydantic1.10.11 41.64% <3.40%> (-1.06%) ⬇️
unit-tests-pandas-macOS-py3.13-pandas3.0.0-pydantic2.12.3 41.79% <3.40%> (-1.07%) ⬇️
unit-tests-pandas-macOS-py3.14-pandas2.3.3-pydantic2.12.3 41.76% <3.40%> (-1.07%) ⬇️
unit-tests-pandas-macOS-py3.14-pandas3.0.0-pydantic2.12.3 41.77% <3.40%> (-1.07%) ⬇️
unit-tests-polars-Linux-py3.10-pandas2.3.3-polars1.33.1 30.28% <0.63%> (-0.81%) ⬇️
unit-tests-polars-Linux-py3.11-pandas2.3.3-polars1.33.1 30.26% <0.63%> (-0.81%) ⬇️
unit-tests-polars-Linux-py3.12-pandas2.3.3-polars1.33.1 30.26% <0.63%> (-0.81%) ⬇️
unit-tests-polars-Linux-py3.13-pandas2.3.3-polars1.33.1 30.26% <0.63%> (-0.81%) ⬇️
unit-tests-polars-Linux-py3.14-pandas2.3.3-polars1.33.1 30.14% <0.63%> (-0.81%) ⬇️
unit-tests-polars-Windows-py3.10-pandas2.3.3-polars1.33.1 30.20% <0.63%> (-0.81%) ⬇️
unit-tests-polars-Windows-py3.11-pandas2.3.3-polars1.33.1 30.19% <0.63%> (-0.81%) ⬇️
unit-tests-polars-Windows-py3.12-pandas2.3.3-polars1.33.1 30.19% <0.63%> (-0.81%) ⬇️
unit-tests-polars-Windows-py3.13-pandas2.3.3-polars1.33.1 30.19% <0.63%> (-0.81%) ⬇️
unit-tests-polars-Windows-py3.14-pandas2.3.3-polars1.33.1 30.07% <0.63%> (-0.81%) ⬇️
unit-tests-polars-macOS-py3.10-pandas2.3.3-polars1.33.1 30.22% <0.63%> (-0.73%) ⬇️
unit-tests-polars-macOS-py3.11-pandas2.3.3-polars1.33.1 30.20% <0.63%> (-0.73%) ⬇️
unit-tests-pyspark-macOS-py3.10-pandas2.3.3-polars 37.95% <2.97%> (-0.93%) ⬇️
unit-tests-strategies-Linux-py3.12-pandas3.0.0-pydantic2.12.3 33.37% <2.97%> (-0.82%) ⬇️
unit-tests-xarray-Linux-py3.10-pandas2.3.3-polars 30.98% <0.63%> (-0.75%) ⬇️
unit-tests-xarray-Linux-py3.11-pandas2.3.3-polars 30.98% <0.63%> (-0.75%) ⬇️
unit-tests-xarray-Linux-py3.12-pandas2.3.3-polars 30.98% <0.63%> (-0.75%) ⬇️
unit-tests-xarray-Linux-py3.13-pandas2.3.3-polars 30.98% <0.63%> (-0.75%) ⬇️
unit-tests-xarray-Linux-py3.14-pandas2.3.3-polars 30.91% <0.63%> (-0.75%) ⬇️
unit-tests-xarray-Windows-py3.10-pandas2.3.3-polars 30.91% <0.63%> (-0.75%) ⬇️
unit-tests-xarray-Windows-py3.11-pandas2.3.3-polars 30.91% <0.63%> (-0.75%) ⬇️
unit-tests-xarray-Windows-py3.12-pandas2.3.3-polars 30.91% <0.63%> (-0.75%) ⬇️
unit-tests-xarray-Windows-py3.13-pandas2.3.3-polars 30.91% <0.63%> (-0.75%) ⬇️
unit-tests-xarray-Windows-py3.14-pandas2.3.3-polars 30.84% <0.63%> (-0.75%) ⬇️
unit-tests-xarray-macOS-py3.10-pandas2.3.3-polars 30.92% <0.63%> (-0.75%) ⬇️

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.

cosmicBboy and others added 2 commits July 16, 2026 09:48
Route pd.DataFrame validation through the shared narwhals backend when
PANDERA_USE_NARWHALS_BACKEND=True, following the registration pattern
established for polars, ibis, and pyspark:

- register_pandas_backends() gains a use_narwhals_backend flag that
  force-overrides only the pd.DataFrame entries for DataFrameSchema
  (including the deprecated root-level class), Column, and
  narwhals-frame Check dispatch. SeriesSchema, Index, MultiIndex,
  Parser, Hypothesis, and other pandas-like frame types (dask, modin,
  geopandas, pyspark.pandas) stay on the native backends.
- re_register_pandas_backends() replays fqn-parameterized registrations
  when the flag is toggled at runtime; set_config re-registration and
  clear_narwhals_compatible_backend_registry now cover pandas.
- pandas Column gains the selector property the narwhals backend
  depends on (mirroring polars/ibis/pyspark).
- check_dtype compares native pandas dtypes through the pandas engine
  (schema-driven dispatch like the existing pyspark case) so Column(str)
  accepts object columns and nullable/categorical/tz dtypes keep native
  semantics.
- The check backend passes pd.Series / pd.DataFrame to native=True check
  functions and normalizes Series/DataFrame/numpy outputs, so existing
  pandas-style checks work unchanged.
- ignore_na now ORs in the input column's nullness on pandas-like frames
  (pandas NaN comparisons yield False instead of propagating null);
  polars/ibis behavior is unchanged.
- Failure cases for pandas validations are built and concatenated as
  pandas DataFrames (including all-scalar error sets), with no pandas
  index leakage; scalar failure cases no longer require polars on
  polars-free installs.
- Index components emit a SchemaWarning and are skipped (narwhals has no
  index concept); column-level coerce=True keeps the shared no-op
  warning behavior.

Tests: the tests/narwhals/backends parity suite now runs against pandas
as a third backend; new registration and pandas-specific e2e suites
cover dtype semantics, pandas-style checks, NaN ignore_na handling,
failure-case types, and runtime flag toggling.

Docs: narwhals_backend.md gains a "pandas: differences from the native
backend" section; configuration.md, supported_libraries.md, the API
reference, and config docstrings now mention pandas.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: cosmicBboy <niels.bantilan@gmail.com>
… CI matrix

- pandas >= 3.0 reports __module__ as "pandas" (not "pandas.core.*") for
  its public classes; pandas-like detection in the narwhals check backend
  now compares the root module segment (fix landed in the feature commit).
- test_lazy_failure_cases_scalar_only: string columns are dtype "object"
  on pandas < 3 and "str" on pandas >= 3 — derive the expected dtype name
  from the frame instead of hardcoding "object".
- test_schemas_on_pyspark_pandas.py::test_failure_cases: accept both
  failure-cases shapes — the native pandas backend reshapes into a
  "failure_case" column while the narwhals backend (flag-on CI session)
  returns failing rows with original column names.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: cosmicBboy <niels.bantilan@gmail.com>
@cosmicBboy
cosmicBboy force-pushed the narwhals-pandas-backend branch from c46461b to 1306a92 Compare July 16, 2026 13:49
cosmicBboy and others added 3 commits July 16, 2026 11:01
- Add tests for pandas-style checks returning boolean DataFrames (with
  and without CHECK_OUTPUT_KEY), iterable dtype-check results, scalar
  failure cases on polars-free installs, non-frame err.data fallback in
  failure_cases_metadata, drop_invalid_rows on nullable violations, and
  the _is_pandas_like helper.
- Mark ImportError guards that no CI environment can reach (pandas /
  narwhals are always co-installed there) as no-cover, following the
  existing pyspark register precedent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: cosmicBboy <niels.bantilan@gmail.com>
Because pandas frames are eager, the Narwhals pandas path can reach feature
parity with the native pandas backend. Almost every step is now handled
Narwhals-native; only Index/MultiIndex validation+coercion (Narwhals has no
index concept) and Hypothesis checks (scipy) are delegated to the native
pandas backend.

- Index/MultiIndex components are validated (and coerced) by delegating to the
  native Index/MultiIndex backends; Narwhals preserves the pandas index
  through its operations, so the parsed frame still carries it. Non-pandas
  frames with an index component still warn and skip.
- Column-/schema-level coerce=True uses a hybrid strategy: plain numpy dtypes
  cast via nw.cast, pandas extension dtypes (nullable Int64, Categorical,
  tz-aware datetime, string) fall back to the pandas dtype engine so native
  semantics are preserved. Cast failures report DATATYPE_COERCION with the
  offending values. Column-level coerce is no longer a no-op warning on pandas.
- add_missing_columns, set_default, and custom parsers are applied
  Narwhals-native (parsers run on the native frame pre-wrap); added columns
  follow the same hybrid dtype rule as coercion.
- groupby column-check-groups run Narwhals-native via
  NarwhalsCheckBackend.apply_groupby (builds the pandas group dict); Hypothesis
  checks are delegated to the native pandas hypothesis backend.
- unique_column_names is detected on the native frame pre-wrap (Narwhals
  rejects duplicate labels at wrap time).

Docs updated to describe the pandas parity and remaining delegated gaps. Adds
extensive e2e coverage in tests/narwhals/test_pandas_e2e.py (index/multiindex
validation+coercion, hybrid coercion fidelity, parsers, add_missing_columns,
set_default, groupby, hypothesis, unique column names).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- test_schema_level_coerce: derive the expected str-coercion dtype from
  the pandas engine instead of hardcoding `object`. On pandas >= 3.0
  `Column(str)` coerces to `pd.StringDtype(na_value=nan)`, not `object`,
  so the hardcoded assertion failed on the pandas-3.0 narwhals CI.
- test_set_defaults: drop the now-stale strict xfail for the narwhals
  backend. set_default is implemented for the narwhals backend, so the
  test XPASSed and failed the strict-xfail gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cosmicBboy
cosmicBboy merged commit 85cc2a1 into main Jul 31, 2026
645 of 646 checks passed
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