Skip to content

fix: propagate nested Column drop_invalid_rows without mutation - #2551

Merged
cosmicbboy merged 2 commits into
unionai-oss:mainfrom
cosmicbboy:fix/nested-drop-invalid-rows-non-mutating
Oct 8, 2026
Merged

cosmicbboy merged 2 commits into
unionai-oss:mainfrom
cosmicbboy:fix/nested-drop-invalid-rows-non-mutating

Conversation

@cosmicbboy

Copy link
Copy Markdown
Collaborator

Summary

A Column(drop_invalid_rows=True) nested inside a DataFrameSchema drops its own failing rows and returns a filtered frame instead of raising, but the container backends discarded that return value, so invalid rows survived in the validated output (issue #2529).

This PR fixes that with a non-mutating, uniform implementation and replaces the pandas in-place approach from PR #2521.

What changed

  • pandas (backends/pandas/container.py): reverts the in-place check_obj.drop(..., inplace=True) mutation from fix(pandas): propagate a column's own drop_invalid_rows through DataFrameSchema #2521; the filtered frame returned by a drop_invalid_rows component is now threaded through the container's local check_obj binding instead — the caller's frame is never mutated.
  • polars / narwhals (covers pyarrow): lazy frames are immutable, so the filtered frame is threaded explicitly through the container's local lazy frame and recovered into the returned output. Skipped under head/tail/sample subsampling because a dropped subsample cannot be mapped back onto the full frame (matching the earlier pandas behavior).

The abstract method signature

run_schema_component_checks(check_obj, schema, schema_components, lazy) -> list[CoreCheckResult]

is unchanged — no widening of the return type, no fragile special-casing in the validate() loop.

Each drop component is validated once for error collection (in run_schema_component_checks) and once to recover its filtered frame; schema-level errors from the second pass are ignored because they are already handled by run_schema_component_checks.

Backend scope

Column-level drop_invalid_rows is implemented in the pandas, polars, pyarrow (via narwhals) backends — those are covered and now fixed. pyspark, xarray, tensordict do not expose column-level drop_invalid_rows. The ibis API exposes the flag but the ibis backend component validate() has no drop path (it only raises), so implementing nested drop there is a separate feature, not a propagation fix — left out of scope.

Tests

Regression tests for pandas, polars, narwhals, pyarrow assert a nested drop_invalid_rows column keeps dropping its failing rows (propagating to the full output row across all columns) even when the DataFrameSchema itself does not set drop_invalid_rows, plus a pandas guard that the caller's frame is not mutated.

Backward compatibility

No existing tests were modified except removing #2521's in-place mutation (replaced with the non-mutating propagation) and adding new regression tests. User-visible outputs are preserved: the #2521 pandas regression test still passes unmodified. The only change is removing reliance on mutating the caller's frame, which is an internal implementation detail, not part of the public contract.

A Column(drop_invalid_rows=True) nested inside a DataFrameSchema drops its
own failing rows and returns a filtered frame instead of raising, but the
container backends discarded that return value, so invalid rows survived in
the validated output (issue unionai-oss#2529).

This replaces the pandas in-place approach from PR unionai-oss#2521 with a non-mutating,
uniform implementation across the backends that support column-level
drop_invalid_rows:

- pandas: revert the in-place check_obj.drop(..., inplace=True) mutation
  and instead thread the filtered frame through the container's local
  check_obj binding.
- polars / narwhals (covers pyarrow): lazy frames are immutable, so the
  filtered frame returned by a drop_invalid_rows component's validate() is
  threaded explicitly through the container's local lazy frame and recovered
  into the returned output. Skipped under head/tail/sample subsampling because
  a dropped subsample cannot be mapped back onto the full frame (matching the
  earlier pandas behavior).

The abstract run_schema_component_checks(check_obj, schema, components,
lazy) -> list[CoreCheckResult] signature is unchanged. Each drop component is
validated once for error collection and once to recover its filtered frame;
schema-level errors are ignored during the latter because they are already
handled by run_schema_component_checks.

Backends without column-level drop_invalid_rows (pyspark, ibis, xarray,
tensordict) are out of scope: the pandas column API exposes the flag in the
ibis API but the ibis backend does not implement a drop path.

Adds regression tests for pandas, polars, narwhals and pyarrow asserting the
nested column keeps dropping its failing rows (propagating to the full output
row across all columns) even when the DataFrameSchema itself does not set
drop_invalid_rows, plus a pandas guard that the caller's frame is not mutated.

## Backward compatibility

No existing tests were modified except removing the in-place mutation from PR
unionai-oss#2521's pandas implementation and adding new regression tests. User-visible
outputs are preserved for pandas (the unionai-oss#2521 regression test still passes); the
change only removes reliance on mutating the caller's frame, which is an
internal implementation detail and not part of the public contract.
@codecov

codecov Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.33333% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.59%. Comparing base (055ace2) to head (c3923ec).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
pandera/backends/pandas/container.py 76.92% 3 Missing ⚠️
pandera/backends/narwhals/container.py 86.66% 2 Missing ⚠️
pandera/backends/polars/container.py 85.71% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2551      +/-   ##
==========================================
+ Coverage   82.57%   82.59%   +0.02%     
==========================================
  Files         230      230              
  Lines       20729    20766      +37     
==========================================
+ Hits        17116    17151      +35     
- Misses       3613     3615       +2     
Flag Coverage Δ
unit-tests-narwhals-Linux-py3.10 34.54% <35.71%> (+0.01%) ⬆️
unit-tests-narwhals-Linux-py3.11 34.53% <35.71%> (+0.01%) ⬆️
unit-tests-narwhals-Linux-py3.12 34.53% <35.71%> (+0.01%) ⬆️
unit-tests-narwhals-Linux-py3.13 34.53% <35.71%> (+0.01%) ⬆️
unit-tests-narwhals-Linux-py3.14 34.39% <35.71%> (+0.02%) ⬆️
unit-tests-narwhals-backend-ibis-py3.10 27.98% <19.04%> (-0.02%) ⬇️
unit-tests-narwhals-backend-ibis-py3.11 27.88% <19.04%> (-0.02%) ⬇️
unit-tests-narwhals-backend-ibis-py3.12 27.88% <19.04%> (-0.02%) ⬇️
unit-tests-narwhals-backend-ibis-py3.13 27.88% <19.04%> (-0.02%) ⬇️
unit-tests-narwhals-backend-ibis-py3.14 27.71% <19.04%> (-0.02%) ⬇️
unit-tests-narwhals-backend-polars-py3.10 31.39% <35.71%> (+0.04%) ⬆️
unit-tests-narwhals-backend-polars-py3.11 31.38% <35.71%> (+0.04%) ⬆️
unit-tests-narwhals-backend-polars-py3.12 31.38% <35.71%> (+0.04%) ⬆️
unit-tests-narwhals-backend-polars-py3.13 31.38% <35.71%> (+0.04%) ⬆️
unit-tests-narwhals-backend-polars-py3.14 31.26% <35.71%> (+0.04%) ⬆️
unit-tests-narwhals-backend-pyspark-py3.10 35.32% <23.80%> (-0.03%) ⬇️
unit-tests-narwhals-backend-pyspark-py3.11 35.35% <23.80%> (-0.03%) ⬇️
unit-tests-pandas-Linux-py3.10-pandas2.3.3-pydantic1.10.11 36.12% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-Linux-py3.10-pandas2.3.3-pydantic2.12.3 36.30% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-Linux-py3.11-pandas2.3.3-pydantic1.10.11 36.12% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-Linux-py3.11-pandas2.3.3-pydantic2.12.3 36.30% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-Linux-py3.11-pandas3.0.0-pydantic1.10.11 36.13% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-Linux-py3.11-pandas3.0.0-pydantic2.12.3 36.30% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-Linux-py3.12-pandas2.3.3-pydantic1.10.11 36.11% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-Linux-py3.12-pandas2.3.3-pydantic2.12.3 36.29% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-Linux-py3.12-pandas3.0.0-pydantic1.10.11 36.12% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-Linux-py3.12-pandas3.0.0-pydantic2.12.3 36.29% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-Linux-py3.13-pandas2.3.3-pydantic1.10.11 36.11% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-Linux-py3.13-pandas2.3.3-pydantic2.12.3 36.29% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-Linux-py3.13-pandas3.0.0-pydantic1.10.11 36.12% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-Linux-py3.13-pandas3.0.0-pydantic2.12.3 36.29% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-Linux-py3.14-pandas2.3.3-pydantic2.12.3 36.26% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-Linux-py3.14-pandas3.0.0-pydantic2.12.3 36.27% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-Windows-py3.10-pandas2.3.3-pydantic1.10.11 36.05% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-Windows-py3.10-pandas2.3.3-pydantic2.12.3 36.23% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-Windows-py3.11-pandas2.3.3-pydantic1.10.11 36.05% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-Windows-py3.11-pandas2.3.3-pydantic2.12.3 36.23% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-Windows-py3.11-pandas3.0.0-pydantic1.10.11 36.06% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-Windows-py3.11-pandas3.0.0-pydantic2.12.3 36.23% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-Windows-py3.12-pandas2.3.3-pydantic1.10.11 36.04% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-Windows-py3.12-pandas2.3.3-pydantic2.12.3 36.22% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-Windows-py3.12-pandas3.0.0-pydantic1.10.11 36.05% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-Windows-py3.12-pandas3.0.0-pydantic2.12.3 36.22% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-Windows-py3.13-pandas2.3.3-pydantic1.10.11 36.04% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-Windows-py3.13-pandas2.3.3-pydantic2.12.3 36.22% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-Windows-py3.13-pandas3.0.0-pydantic1.10.11 36.05% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-Windows-py3.13-pandas3.0.0-pydantic2.12.3 36.22% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-Windows-py3.14-pandas2.3.3-pydantic2.12.3 36.19% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-Windows-py3.14-pandas3.0.0-pydantic2.12.3 36.19% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-macOS-py3.10-pandas2.3.3-pydantic1.10.11 36.07% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-macOS-py3.10-pandas2.3.3-pydantic2.12.3 36.25% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-macOS-py3.11-pandas2.3.3-pydantic1.10.11 36.07% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-macOS-py3.11-pandas2.3.3-pydantic2.12.3 36.25% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-macOS-py3.11-pandas3.0.0-pydantic1.10.11 36.08% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-macOS-py3.11-pandas3.0.0-pydantic2.12.3 36.25% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-macOS-py3.12-pandas2.3.3-pydantic1.10.11 36.06% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-macOS-py3.12-pandas2.3.3-pydantic2.12.3 36.24% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-macOS-py3.12-pandas3.0.0-pydantic1.10.11 36.07% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-macOS-py3.12-pandas3.0.0-pydantic2.12.3 36.24% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-macOS-py3.13-pandas2.3.3-pydantic1.10.11 36.06% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-macOS-py3.13-pandas2.3.3-pydantic2.12.3 36.24% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-macOS-py3.13-pandas3.0.0-pydantic1.10.11 36.07% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-macOS-py3.13-pandas3.0.0-pydantic2.12.3 36.24% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-macOS-py3.14-pandas2.3.3-pydantic2.12.3 36.21% <23.80%> (-0.04%) ⬇️
unit-tests-pandas-macOS-py3.14-pandas3.0.0-pydantic2.12.3 36.22% <23.80%> (-0.04%) ⬇️
unit-tests-polars-Linux-py3.10-pandas2.3.3-polars1.20.0 26.53% <28.57%> (+0.01%) ⬆️
unit-tests-polars-Linux-py3.11-pandas2.3.3-polars1.20.0 26.52% <28.57%> (+0.01%) ⬆️
unit-tests-polars-Linux-py3.12-pandas2.3.3-polars1.33.1 26.52% <28.57%> (+0.01%) ⬆️
unit-tests-polars-Linux-py3.13-pandas2.3.3-polars1.33.1 26.52% <28.57%> (+0.01%) ⬆️
unit-tests-polars-Linux-py3.14-pandas2.3.3-polars1.42.1 26.42% <28.57%> (+0.02%) ⬆️
unit-tests-polars-Windows-py3.10-pandas2.3.3-polars1.20.0 26.47% <28.57%> (+0.01%) ⬆️
unit-tests-polars-Windows-py3.11-pandas2.3.3-polars1.20.0 26.46% <28.57%> (+0.01%) ⬆️
unit-tests-polars-Windows-py3.12-pandas2.3.3-polars1.33.1 26.46% <28.57%> (+0.01%) ⬆️
unit-tests-polars-Windows-py3.13-pandas2.3.3-polars1.33.1 26.46% <28.57%> (+0.01%) ⬆️
unit-tests-polars-Windows-py3.14-pandas2.3.3-polars1.42.1 26.36% <28.57%> (+0.02%) ⬆️
unit-tests-polars-macOS-py3.10-pandas2.3.3-polars1.20.0 26.48% <28.57%> (+0.01%) ⬆️
unit-tests-polars-macOS-py3.11-pandas2.3.3-polars1.20.0 26.47% <28.57%> (+0.01%) ⬆️
unit-tests-polars-macOS-py3.12-pandas2.3.3-polars1.33.1 26.47% <28.57%> (-1.68%) ⬇️
unit-tests-polars-macOS-py3.13-pandas2.3.3-polars1.33.1 26.47% <28.57%> (-0.03%) ⬇️
unit-tests-polars-macOS-py3.14-pandas2.3.3-polars1.42.1 26.37% <28.57%> (-1.29%) ⬇️
unit-tests-pyarrow-Linux-py3.10-pandas2.3.3-polars 21.49% <30.95%> (+0.04%) ⬆️
unit-tests-pyarrow-Linux-py3.11-pandas2.3.3-polars 21.49% <30.95%> (+0.04%) ⬆️
unit-tests-pyarrow-Linux-py3.12-pandas2.3.3-polars 21.49% <30.95%> (+0.04%) ⬆️
unit-tests-pyarrow-Linux-py3.13-pandas2.3.3-polars 21.49% <30.95%> (+0.04%) ⬆️
unit-tests-pyarrow-Linux-py3.14-pandas2.3.3-polars 21.36% <30.95%> (+0.04%) ⬆️
unit-tests-pyarrow-Windows-py3.10-pandas2.3.3-polars 21.43% <30.95%> (+0.04%) ⬆️
unit-tests-pyarrow-Windows-py3.11-pandas2.3.3-polars 21.43% <30.95%> (+0.04%) ⬆️
unit-tests-pyarrow-Windows-py3.12-pandas2.3.3-polars 21.43% <30.95%> (+0.04%) ⬆️
unit-tests-pyarrow-Windows-py3.13-pandas2.3.3-polars 21.43% <30.95%> (+0.04%) ⬆️
unit-tests-pyarrow-Windows-py3.14-pandas2.3.3-polars 21.29% <30.95%> (+0.04%) ⬆️
unit-tests-pyarrow-macOS-py3.10-pandas2.3.3-polars 21.44% <30.95%> (-1.37%) ⬇️
unit-tests-pyarrow-macOS-py3.11-pandas2.3.3-polars 21.44% <30.95%> (-1.45%) ⬇️
unit-tests-pyarrow-macOS-py3.12-pandas2.3.3-polars 21.44% <30.95%> (+0.04%) ⬆️
unit-tests-pyarrow-macOS-py3.13-pandas2.3.3-polars 21.44% <30.95%> (+0.04%) ⬆️
unit-tests-pyarrow-macOS-py3.14-pandas2.3.3-polars 21.30% <30.95%> (+<0.01%) ⬆️

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 _ArrayKwargs TypedDict declared a 'width' key that is never set (the
code only ever writes kwargs['shape']). This made mypy type **kwargs as
possibly containing 'width', which newer polars stubs reject with
'Extra argument "width" from **args for Array' when the deprecated arg
was removed. Removing the unused key resolves the mypy error without any
behavior change.
@cosmicbboy
cosmicbboy merged commit eb7b679 into unionai-oss:main Oct 8, 2026
119 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