Skip to content

fix(io): format dataframe-level checks in to_script - #2418

Open
chuenchen309 wants to merge 1 commit into
unionai-oss:mainfrom
chuenchen309:fix/to-script-dataframe-level-checks
Open

fix(io): format dataframe-level checks in to_script#2418
chuenchen309 wants to merge 1 commit into
unionai-oss:mainfrom
chuenchen309:fix/to-script-dataframe-level-checks

Conversation

@chuenchen309

Copy link
Copy Markdown

Problem

to_script(minimal=False) produces a script that runs fine and looks correct, but the schema it rebuilds is unusable — every validate() call on it raises TypeError: 'dict' object is not callable, including on data the original schema accepts:

import pandas as pd
from pandera.pandas import DataFrameSchema, Column, Check

schema = DataFrameSchema({"a": Column(int)}, checks=[Check.gt(0)])
ns = {}
exec(schema.to_script(minimal=False), ns)
rt = ns["schema"]

print(rt.checks)
# [{'min_value': 0, 'options': {'check_name': 'greater_than', ...}}]   <- dicts, not Checks
print(schema == rt)
# False

schema.validate(pd.DataFrame({"a": [5]}))   # passes, as it should
rt.validate(pd.DataFrame({"a": [5]}))       # SchemaError: Error while executing check
                                            # function: TypeError("'dict' object is not callable")

Column-level checks round-trip correctly, so the two paths disagree:

s2 = DataFrameSchema({"a": Column(int, Check.gt(0))})   # same check, column level
ns2 = {}; exec(s2.to_script(minimal=False), ns2)
print(ns2["schema"].columns["a"].checks)   # [<Check greater_than: greater_than(0)>]
print(s2 == ns2["schema"])                 # True

The failure is quiet at the point of corruption: the generated script is valid Python and the DataFrameSchema(...) call succeeds. It only surfaces later, as a confusing TypeError from inside validation that gives no hint that to_script was the culprit.

Cause

In to_script the column path formats its checks, but the dataframe-level path passes the raw statistics list straight into the template — 19 lines apart in the same function:

column_code = COLUMN_TEMPLATE.format(
    checks=_format_checks(properties["checks"]),   # line 820 — formatted
    ...
)
...
script = SCRIPT_TEMPLATE.format(
    checks=statistics["checks"],                   # line 841 — raw dicts
    ...
)

Every other caller of the template data (_format_index, _format_column_minimal, the column path) goes through _format_checks; this one line is the exception.

Fix

Format dataframe-level checks the same way the column path does. _format_checks(None) already returns "None", so schemas without dataframe-level checks are unaffected.

After the fix, rt.checks is [<Check greater_than: greater_than(0)>], schema == rt is True, and the round-tripped schema accepts and rejects exactly what the original does.

Testing

  • Added test_to_script_dataframe_level_checks to tests/io/test_pandas_io.py. The existing test_to_script asserts the right invariant (schema == schema_to_write) but its _create_schema() fixture defines only column checks, so this path had no coverage. Verified the new test fails before the fix and passes after.
  • tests/io/ → 73 passed. The 5 test_frictionless_* failures are pre-existing: they fail identically on an unmodified main in my environment because frictionless isn't installed, and are unrelated to this change.
  • tests/pandas/test_schema_statistics.py + tests/pandas/test_schema_inference.py → 80 passed, 1 skipped (to_script goes through get_dataframe_schema_statistics).
  • ruff check (the linter CI runs) and ruff format --check clean on both changed files.

Note on #2402

Heads-up that #2402 moves to_script/SCRIPT_TEMPLATE/_format_checks into pandera/io/common_io.py and carries checks=statistics["checks"] across unchanged, so the bug would survive that refactor. Happy to rebase onto it, or to re-send against common_io.py instead if you'd rather land #2402 first — whichever is less work for you.

AI disclosure

Prepared with AI assistance (Claude Code). I reproduced the bug against the unmodified library, verified the fix and the test locally, and take responsibility for the change.

to_script(minimal=False) interpolated the raw statistics list for
dataframe-level checks straight into the script template, while the
column path 19 lines above formats its checks with _format_checks.

The generated script runs and looks correct, but the schema it builds
carries plain dicts where Check objects belong. Every validate() call on
it then fails with TypeError("'dict' object is not callable"), including
on data the original schema accepts, so the round-tripped schema is
unusable rather than merely wrong.

Format dataframe-level checks the same way the column path does.
_format_checks(None) already returns "None", so schemas without
dataframe-level checks are unaffected.

The existing test_to_script only covers column checks, so this path was
untested; add a round-trip test with a dataframe-level check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Andrew Chen <48723787+chuenchen309@users.noreply.github.com>
@chuenchen309
chuenchen309 force-pushed the fix/to-script-dataframe-level-checks branch from 0af3e08 to 8056883 Compare July 16, 2026 12:52
@codecov

codecov Bot commented Jul 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.21%. Comparing base (9d86aed) to head (8056883).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #2418   +/-   ##
=======================================
  Coverage   91.21%   91.21%           
=======================================
  Files         193      193           
  Lines       17135    17135           
=======================================
  Hits        15629    15629           
  Misses       1506     1506           
Flag Coverage Δ
unit-tests-base-Linux-py3.10 15.38% <ø> (ø)
unit-tests-base-Linux-py3.11 15.38% <ø> (ø)
unit-tests-base-Linux-py3.12 15.38% <ø> (ø)
unit-tests-base-Linux-py3.13 15.38% <ø> (ø)
unit-tests-base-Linux-py3.14 15.24% <ø> (ø)
unit-tests-base-Windows-py3.10 15.30% <ø> (ø)
unit-tests-base-Windows-py3.11 15.30% <ø> (ø)
unit-tests-base-Windows-py3.12 15.30% <ø> (ø)
unit-tests-base-Windows-py3.13 15.30% <ø> (ø)
unit-tests-base-Windows-py3.14 15.17% <ø> (ø)
unit-tests-base-macOS-py3.10 15.31% <ø> (ø)
unit-tests-base-macOS-py3.11 15.31% <ø> (ø)
unit-tests-base-macOS-py3.12 15.31% <ø> (ø)
unit-tests-base-macOS-py3.13 15.31% <ø> (ø)
unit-tests-base-macOS-py3.14 15.18% <ø> (ø)
unit-tests-dask-Linux-py3.10-pandas2.3.3-polars 24.23% <ø> (ø)
unit-tests-dask-Linux-py3.11-pandas2.3.3-polars 24.23% <ø> (ø)
unit-tests-dask-Linux-py3.12-pandas2.3.3-polars 24.23% <ø> (ø)
unit-tests-dask-Linux-py3.13-pandas2.3.3-polars 24.23% <ø> (ø)
unit-tests-dask-Linux-py3.14-pandas2.3.3-polars 24.14% <ø> (ø)
unit-tests-dask-Windows-py3.10-pandas2.3.3-polars 24.14% <ø> (ø)
unit-tests-dask-Windows-py3.11-pandas2.3.3-polars 24.14% <ø> (ø)
unit-tests-dask-Windows-py3.12-pandas2.3.3-polars 24.14% <ø> (ø)
unit-tests-dask-Windows-py3.13-pandas2.3.3-polars 24.14% <ø> (ø)
unit-tests-dask-Windows-py3.14-pandas2.3.3-polars 24.06% <ø> (ø)
unit-tests-dask-macOS-py3.10-pandas2.3.3-polars 24.17% <ø> (ø)
unit-tests-dask-macOS-py3.11-pandas2.3.3-polars 24.17% <ø> (-0.03%) ⬇️
unit-tests-fastapi-Linux-py3.11-pandas3.0.0-pydantic2.12.3 24.74% <ø> (-0.03%) ⬇️
unit-tests-fastapi-Linux-py3.12-pandas3.0.0-pydantic2.12.3 24.74% <ø> (ø)
unit-tests-geopandas-Linux-py3.10-pandas2.3.3-pydantic2.12.3 30.04% <ø> (-0.04%) ⬇️
unit-tests-geopandas-Linux-py3.11-pandas2.3.3-pydantic2.12.3 30.04% <ø> (ø)
unit-tests-geopandas-Linux-py3.12-pandas3.0.0-pydantic2.12.3 30.04% <ø> (ø)
unit-tests-hypotheses-Linux-py3.10-pandas2.3.3-pydantic2.12.3 22.48% <ø> (+<0.01%) ⬆️
unit-tests-hypotheses-Linux-py3.14-pandas2.3.3-pydantic2.12.3 22.37% <ø> (-0.03%) ⬇️
unit-tests-hypotheses-Windows-py3.11-pandas2.3.3-pydantic2.12.3 22.40% <ø> (-0.03%) ⬇️
unit-tests-ibis-Linux-py3.10-pandas2.3.3-polars 28.85% <ø> (ø)
unit-tests-ibis-Linux-py3.11-pandas2.3.3-polars 28.85% <ø> (ø)
unit-tests-ibis-Linux-py3.12-pandas2.3.3-polars 28.85% <ø> (ø)
unit-tests-ibis-Linux-py3.13-pandas2.3.3-polars 28.85% <ø> (ø)
unit-tests-ibis-Linux-py3.14-pandas2.3.3-polars 28.66% <ø> (ø)
unit-tests-ibis-Windows-py3.10-pandas2.3.3-polars 28.78% <ø> (ø)
unit-tests-ibis-Windows-py3.11-pandas2.3.3-polars 28.78% <ø> (ø)
unit-tests-ibis-Windows-py3.12-pandas2.3.3-polars 28.78% <ø> (ø)
unit-tests-ibis-Windows-py3.13-pandas2.3.3-polars 28.78% <ø> (ø)
unit-tests-ibis-Windows-py3.14-pandas2.3.3-polars 28.59% <ø> (ø)
unit-tests-ibis-macOS-py3.10-pandas2.3.3-polars 28.79% <ø> (+<0.01%) ⬆️
unit-tests-ibis-macOS-py3.11-pandas2.3.3-polars 28.79% <ø> (+<0.01%) ⬆️
unit-tests-ibis-macOS-py3.13-pandas2.3.3-polars 28.79% <ø> (-0.01%) ⬇️
unit-tests-io-Linux-py3.10-pandas2.3.3-pydantic2.12.3 38.24% <ø> (+0.14%) ⬆️
unit-tests-io-Linux-py3.11-pandas2.3.3-pydantic2.12.3 38.27% <ø> (+0.14%) ⬆️
unit-tests-io-Linux-py3.12-pandas2.3.3-pydantic2.12.3 38.27% <ø> (+0.14%) ⬆️
unit-tests-io-Linux-py3.13-pandas2.3.3-pydantic2.12.3 38.27% <ø> (+0.14%) ⬆️
unit-tests-io-Linux-py3.14-pandas2.3.3-pydantic2.12.3 38.17% <ø> (+0.15%) ⬆️
unit-tests-modin-ray-Linux-py3.10-pandas2.3.3-polars 30.64% <ø> (ø)
unit-tests-modin-ray-Linux-py3.11-pandas2.3.3-polars 30.64% <ø> (ø)
unit-tests-modin-ray-Linux-py3.12-pandas2.3.3-polars 30.64% <ø> (ø)
unit-tests-modin-ray-macOS-py3.10-pandas2.3.3-polars 30.58% <ø> (-0.04%) ⬇️
unit-tests-modin-ray-macOS-py3.11-pandas2.3.3-polars 30.58% <ø> (-0.04%) ⬇️
unit-tests-narwhals-Linux-py3.10 28.26% <ø> (ø)
unit-tests-narwhals-Linux-py3.11 28.26% <ø> (ø)
unit-tests-narwhals-Linux-py3.12 28.26% <ø> (ø)
unit-tests-narwhals-Linux-py3.13 28.26% <ø> (ø)
unit-tests-narwhals-Linux-py3.14 28.04% <ø> (ø)
unit-tests-narwhals-backend-ibis-py3.10 31.80% <ø> (ø)
unit-tests-narwhals-backend-ibis-py3.11 31.69% <ø> (ø)
unit-tests-narwhals-backend-ibis-py3.12 31.69% <ø> (ø)
unit-tests-narwhals-backend-ibis-py3.13 31.69% <ø> (ø)
unit-tests-narwhals-backend-ibis-py3.14 31.48% <ø> (ø)
unit-tests-narwhals-backend-polars-py3.10 34.48% <ø> (ø)
unit-tests-narwhals-backend-polars-py3.11 34.47% <ø> (ø)
unit-tests-narwhals-backend-polars-py3.12 34.47% <ø> (ø)
unit-tests-narwhals-backend-polars-py3.13 34.47% <ø> (ø)
unit-tests-narwhals-backend-polars-py3.14 34.33% <ø> (ø)
unit-tests-narwhals-backend-pyspark-py3.11 43.49% <ø> (ø)
unit-tests-pandas-Linux-py3.10-pandas2.3.3-pydantic1.10.11 42.76% <ø> (ø)
unit-tests-pandas-Linux-py3.10-pandas2.3.3-pydantic2.12.3 42.91% <ø> (ø)
unit-tests-pandas-Linux-py3.11-pandas2.3.3-pydantic1.10.11 42.76% <ø> (ø)
unit-tests-pandas-Linux-py3.11-pandas2.3.3-pydantic2.12.3 42.91% <ø> (ø)
unit-tests-pandas-Linux-py3.11-pandas3.0.0-pydantic1.10.11 42.77% <ø> (ø)
unit-tests-pandas-Linux-py3.11-pandas3.0.0-pydantic2.12.3 42.93% <ø> (ø)
unit-tests-pandas-Linux-py3.12-pandas2.3.3-pydantic1.10.11 42.75% <ø> (ø)
unit-tests-pandas-Linux-py3.12-pandas2.3.3-pydantic2.12.3 42.90% <ø> (ø)
unit-tests-pandas-Linux-py3.12-pandas3.0.0-pydantic1.10.11 42.76% <ø> (ø)
unit-tests-pandas-Linux-py3.12-pandas3.0.0-pydantic2.12.3 42.91% <ø> (ø)
unit-tests-pandas-Linux-py3.13-pandas2.3.3-pydantic1.10.11 42.75% <ø> (ø)
unit-tests-pandas-Linux-py3.13-pandas2.3.3-pydantic2.12.3 42.90% <ø> (ø)
unit-tests-pandas-Linux-py3.13-pandas3.0.0-pydantic1.10.11 42.76% <ø> (ø)
unit-tests-pandas-Linux-py3.13-pandas3.0.0-pydantic2.12.3 42.91% <ø> (ø)
unit-tests-pandas-Linux-py3.14-pandas2.3.3-pydantic2.12.3 42.88% <ø> (ø)
unit-tests-pandas-Linux-py3.14-pandas3.0.0-pydantic2.12.3 42.90% <ø> (ø)
unit-tests-pandas-Windows-py3.10-pandas2.3.3-pydantic1.10.11 42.67% <ø> (ø)
unit-tests-pandas-Windows-py3.10-pandas2.3.3-pydantic2.12.3 42.83% <ø> (ø)
unit-tests-pandas-Windows-py3.11-pandas2.3.3-pydantic1.10.11 42.67% <ø> (ø)
unit-tests-pandas-Windows-py3.11-pandas2.3.3-pydantic2.12.3 42.83% <ø> (ø)
unit-tests-pandas-Windows-py3.11-pandas3.0.0-pydantic1.10.11 42.68% <ø> (ø)
unit-tests-pandas-Windows-py3.11-pandas3.0.0-pydantic2.12.3 42.84% <ø> (ø)
unit-tests-pandas-Windows-py3.12-pandas2.3.3-pydantic1.10.11 42.66% <ø> (ø)
unit-tests-pandas-Windows-py3.12-pandas2.3.3-pydantic2.12.3 42.81% <ø> (ø)
unit-tests-pandas-Windows-py3.12-pandas3.0.0-pydantic1.10.11 42.67% <ø> (ø)
unit-tests-pandas-Windows-py3.12-pandas3.0.0-pydantic2.12.3 42.83% <ø> (ø)
unit-tests-pandas-Windows-py3.13-pandas2.3.3-pydantic1.10.11 42.66% <ø> (ø)
unit-tests-pandas-Windows-py3.13-pandas2.3.3-pydantic2.12.3 42.81% <ø> (ø)
unit-tests-pandas-Windows-py3.13-pandas3.0.0-pydantic1.10.11 42.67% <ø> (ø)
unit-tests-pandas-Windows-py3.13-pandas3.0.0-pydantic2.12.3 42.83% <ø> (ø)
unit-tests-pandas-Windows-py3.14-pandas2.3.3-pydantic2.12.3 42.80% <ø> (ø)
unit-tests-pandas-Windows-py3.14-pandas3.0.0-pydantic2.12.3 42.81% <ø> (ø)
unit-tests-pandas-macOS-py3.10-pandas2.3.3-pydantic1.10.11 42.70% <ø> (ø)
unit-tests-pandas-macOS-py3.10-pandas2.3.3-pydantic2.12.3 42.85% <ø> (ø)
unit-tests-pandas-macOS-py3.11-pandas2.3.3-pydantic1.10.11 42.70% <ø> (ø)
unit-tests-pandas-macOS-py3.11-pandas2.3.3-pydantic2.12.3 42.85% <ø> (ø)
unit-tests-pandas-macOS-py3.11-pandas3.0.0-pydantic1.10.11 42.71% <ø> (ø)
unit-tests-pandas-macOS-py3.11-pandas3.0.0-pydantic2.12.3 42.86% <ø> (ø)
unit-tests-pandas-macOS-py3.12-pandas2.3.3-pydantic1.10.11 42.68% <ø> (ø)
unit-tests-pandas-macOS-py3.12-pandas2.3.3-pydantic2.12.3 42.84% <ø> (ø)
unit-tests-pandas-macOS-py3.12-pandas3.0.0-pydantic1.10.11 42.70% <ø> (ø)
unit-tests-pandas-macOS-py3.12-pandas3.0.0-pydantic2.12.3 42.85% <ø> (ø)
unit-tests-pandas-macOS-py3.13-pandas2.3.3-pydantic1.10.11 42.68% <ø> (ø)
unit-tests-pandas-macOS-py3.13-pandas2.3.3-pydantic2.12.3 42.84% <ø> (ø)
unit-tests-pandas-macOS-py3.13-pandas3.0.0-pydantic1.10.11 42.70% <ø> (ø)
unit-tests-pandas-macOS-py3.13-pandas3.0.0-pydantic2.12.3 42.85% <ø> (ø)
unit-tests-pandas-macOS-py3.14-pandas2.3.3-pydantic2.12.3 42.82% <ø> (ø)
unit-tests-pandas-macOS-py3.14-pandas3.0.0-pydantic2.12.3 42.83% <ø> (ø)
unit-tests-polars-Linux-py3.10-pandas2.3.3-polars1.33.1 31.08% <ø> (ø)
unit-tests-polars-Linux-py3.11-pandas2.3.3-polars1.33.1 31.07% <ø> (ø)
unit-tests-polars-Linux-py3.12-pandas2.3.3-polars1.33.1 31.07% <ø> (ø)
unit-tests-polars-Linux-py3.13-pandas2.3.3-polars1.33.1 31.07% <ø> (ø)
unit-tests-polars-Linux-py3.14-pandas2.3.3-polars1.33.1 30.94% <ø> (ø)
unit-tests-polars-Windows-py3.10-pandas2.3.3-polars1.33.1 31.01% <ø> (ø)
unit-tests-polars-Windows-py3.11-pandas2.3.3-polars1.33.1 30.99% <ø> (ø)
unit-tests-polars-Windows-py3.12-pandas2.3.3-polars1.33.1 30.99% <ø> (ø)
unit-tests-polars-Windows-py3.13-pandas2.3.3-polars1.33.1 30.99% <ø> (ø)
unit-tests-polars-Windows-py3.14-pandas2.3.3-polars1.33.1 30.87% <ø> (ø)
unit-tests-polars-macOS-py3.10-pandas2.3.3-polars1.33.1 31.02% <ø> (+0.07%) ⬆️
unit-tests-polars-macOS-py3.11-pandas2.3.3-polars1.33.1 31.01% <ø> (+0.07%) ⬆️
unit-tests-pyspark-macOS-py3.10-pandas2.3.3-polars 38.91% <ø> (+0.03%) ⬆️
unit-tests-strategies-Linux-py3.10-pandas2.3.3-pydantic2.12.3 34.15% <ø> (-0.04%) ⬇️
unit-tests-strategies-Linux-py3.13-pandas3.0.0-pydantic2.12.3 34.14% <ø> (ø)
unit-tests-xarray-Linux-py3.10-pandas2.3.3-polars 31.73% <ø> (ø)
unit-tests-xarray-Linux-py3.11-pandas2.3.3-polars 31.73% <ø> (ø)
unit-tests-xarray-Linux-py3.12-pandas2.3.3-polars 31.73% <ø> (ø)
unit-tests-xarray-Linux-py3.13-pandas2.3.3-polars 31.73% <ø> (ø)
unit-tests-xarray-Linux-py3.14-pandas2.3.3-polars 31.65% <ø> (ø)
unit-tests-xarray-Windows-py3.10-pandas2.3.3-polars 31.66% <ø> (ø)
unit-tests-xarray-Windows-py3.11-pandas2.3.3-polars 31.66% <ø> (ø)
unit-tests-xarray-Windows-py3.12-pandas2.3.3-polars 31.66% <ø> (ø)
unit-tests-xarray-Windows-py3.13-pandas2.3.3-polars 31.66% <ø> (ø)
unit-tests-xarray-Windows-py3.14-pandas2.3.3-polars 31.58% <ø> (ø)
unit-tests-xarray-macOS-py3.10-pandas2.3.3-polars 31.67% <ø> (ø)
unit-tests-xarray-macOS-py3.11-pandas2.3.3-polars 31.67% <ø> (-0.04%) ⬇️
unit-tests-xarray-macOS-py3.13-pandas2.3.3-polars 31.67% <ø> (?)

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.

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