Limit pandas to < 3 for DataFrame XComs - #70791
Conversation
Under pandas 3 a DataFrame does not round trip through an XCom the way it does under pandas 2. The public classes moved to the `pandas` namespace, so the same object qualifies as `pandas.DataFrame` rather than `pandas.core.frame.DataFrame` and the serde registry -- keyed on the latter -- does not dispatch to the pandas serializer at all. The caller gets serde's generic "cannot serialize object of type" with nothing pointing at pandas. Beyond that, the dtypes differ: an `object` column reads back as `str` and missing values as `nan` rather than `None`, so the reader's pandas version, not the writer's, decides what a task receives. Declare `pandas<3` where Airflow declares pandas, and back it with a runtime check, since pandas is an optional dependency and the constraint alone cannot be relied on -- a deployment may install pandas 3 directly or pull it in through another package. The check needs the pandas 3 qualname registered to be reachable at all: without `pandas.DataFrame` in the registry the request never reaches this module. It is registered for that reason only, to refuse the value with a message that names pandas and says what to do, not to support it. Reads are refused as well as writes. A payload written under pandas 2 comes back with pandas 3 dtypes, so accepting it would hand the task different data than was pushed, with no signal. Moving from pandas 2 to pandas 3 is a deliberate migration for users to make. Support for it is not dropped, only deferred. Generated-by: Claude Opus 5 (1M context) following the guidelines at https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions
|
Cross-reference: #70558 is where pandas 3 support starts, and it is milestoned for 3.4.0. This PR is the deliberate limit we apply until then. The two are complements rather than alternatives:
The reason for limiting rather than shipping support now is that moving from pandas 2 to pandas 3 should be a deliberate decision by the user, not something an Airflow upgrade does to them. Under pandas 3 the same XCom payload reads back with different dtypes — an So this is a deferral, not a drop. Users who want pandas 3 today can still choose it; they just cannot pass DataFrames through XCom while doing so, and they now get told that explicitly rather than discovering it through an unrelated-looking |
The XCom limit only binds where Airflow declares pandas. Thirteen providers pin pandas independently of common-sql, so a deployment installing any of them could still resolve pandas 3 and reach the runtime refusal rather than being held on a supported version by the constraint. pandas-gbq is deliberately untouched -- a different package, unaffected by this. Provider READMEs and uv.lock are regenerated to match.
The update-providers-build-files hook renders the extras table in each provider's docs/index.rst from its pyproject dependencies. Fourteen providers show the pandas extra, so the pin change widens those tables.
…me-xcoms' into limit-pandas-below-3-for-dataframe-xcoms
There was a problem hiding this comment.
AI Summary
Approach is right and the guard is correctly wired. Three findings, none a correctness bug: two I'd like resolved before this merges, one nit.
What I checked rather than took on trust
- Registration is string-keyed (
task-sdk/src/airflow/sdk/serde/__init__.py:477-483), so the extrapandas.DataFrameentry costs no eager pandas import, and_register()only raises when the same name maps to a different module — no duplicate-registration risk from registering an alias of an already-registered class. - Nothing between
serde.serialize()and the serializer swallows theRuntimeError(task-sdk/src/airflow/sdk/serde/__init__.py:245-246), so the actionable message actually reaches the caller on both the write and the read path rather than being retried into the custom-serializer fallback. - The guard sits after the
isinstance(o, pd.DataFrame)check on write and after thecls is not pd.DataFramecheck on read — both correct placements, and non-DataFrame values are unaffected. - No sibling serializer was missed:
task-sdk/src/airflow/sdk/serde/serializers/pandas.pyis the only DataFrame serializer in the repo.
1. The upper bounds carry no rationale comment, and no tracking-issue link
The project's rule on upper bounds, from contributing-docs/13_airflow_dependencies_and_extras.rst:
We very rarely upper-bind dependencies - only when there is a known conflict with a new or upcoming version of a dependency that breaks the installation or tests (and we always make a comment why we are upper-bounding a dependency).
and providers/AGENTS.md:
Don't upper-bound dependencies by default; add limits only with justification.
45 specifiers across 14 files gain <3, and not one of them carries a comment saying why. AGENTS.md § Tracking issues for deferred work is more specific still — a version cap needs a tracking issue and the full issue URL as a comment at the cap site, precisely so the reference outlives the PR. It also rules out the pointer this PR uses:
Do not write vague forward-looking phrases like "will open a tracking issue" or "to be filed later" in the PR body or in code comments.
"see the follow-up PR linked in the comments" in the description is that phrase, and a GitHub cross-reference to #70558 does not reach whoever opens providers/snowflake/pyproject.toml in six months, or the release manager deciding whether the cap can come off. Inline suggestion on the common-sql extra below; the same one-liner at the other 13 sites would close it.
2. Worth stating deliberately how far the caps reach
The 13 provider caps are the widest part of the diff and the narrowest in benefit. The defect is in DataFrame XCom serde, which lives in task-sdk; the provider pandas extras exist for get_df()-style paths that never go near it. After this, someone on apache-airflow-providers-snowflake[pandas] who never puts a DataFrame in an XCom cannot install pandas 3 at all.
That cost also lands outside this repo's release train. Providers ship from main and support Airflow 2.11 through 3.3, so the cap binds users on Airflow versions that do not have — and will never get — the guard it is protecting. It is a permanent property of those released distributions, not something a later Airflow release can lift.
I take the point in the description that capping only common-sql leaves a gap. But the paragraph right after it concedes the constraint cannot be relied on regardless — pandas can be installed directly or pulled in through another package — and the task-sdk pin is in the dev dependency group, so it never reaches a user environment at all. The runtime guard is what actually closes the hole; the caps buy partial coverage at a cost paid largely by users who are not exposed to the bug. Capping common-sql[pandas] alone, which is what apache-airflow[pandas] resolves to, plus the guard, looks like the narrower trade.
Your call and I'm not blocking on it — but with backport-to-v3-3-test putting these caps into a patch-level provider release, I'd rather see the breadth chosen deliberately than inherited.
3. Nit on the new pandas 2 test
Inline below — it cannot fail without this change. The other two new tests are good; test_pandas_3_dataframe_name_is_registered in particular, since it pins the reachability precondition the entire guard depends on and would otherwise be the silent way this feature rots.
This review was drafted by an AI-assisted tool and
confirmed by an Airflow maintainer. The findings
above are observations, not blockers; an Airflow
maintainer — a real person — will take the next look at the
PR. If you think a finding is mis-applied, please reply on
the PR and a maintainer will weigh in.More on how Airflow handles maintainer review:
contributing-docs/05_pull_requests.rst.
Drafted-by: Claude Opus 5 (1M context); reviewed by @shahar1 before posting
contributing-docs/13_airflow_dependencies_and_extras.rst asks for a comment saying why whenever a dependency is upper-bound, and AGENTS.md asks for the tracking URL in the file rather than only in a PR comment. All 45 specifiers across 15 files landed without either. Add the rationale and a pointer to the pandas 3 support PR above each block, so whoever decides when the cap comes off does not have to reconstruct it. test_pandas_2_dataframe_xcom_still_round_trips passed unchanged on main: nothing read pd.__version__ before this PR, so monkeypatching it to a 2.x value while the installed pandas is already 2.x changed nothing observable, and the round trip is already covered by test_pandas_serializers. Dropped.
|
Thanks @shahar1 — both correct, both fixed.
That is the second vacuous test of mine you have caught in this batch, which is a useful signal about how I was writing them: I was checking that the new code path is reachable rather than that the new behaviour is required. The reverting-the-change test is the right bar. |
Backport failed to create: v3-3-test. View the failure log Run detailsNote: As of Merging PRs targeted for Airflow 3.X In matter of doubt please ask in #release-management Slack channel.
You can attempt to backport this manually by running: cherry_picker 415cb70 v3-3-testThis should apply the commit to the v3-3-test branch and leave the commit in conflict state marking After you have resolved the conflicts, you can continue the backport process by running: cherry_picker --continueIf you don't have cherry-picker installed, see the installation guide. |
A
pandas.DataFramedoes not survive an XCom round trip under pandas 3 the way itdoes under pandas 2, so this version of Airflow constrains pandas to
< 3andrefuses DataFrame XComs at runtime when a newer pandas is installed anyway.
What breaks under pandas 3
The serializer is never reached. pandas 3 exposes its public classes from the
pandasnamespace, so a DataFrame qualifies aspandas.DataFramerather thanpandas.core.frame.DataFrame. The serde registry is keyed on the latter, so it doesnot dispatch to the pandas serializer at all and the caller gets serde's generic
with nothing in it pointing at pandas or at what to do about it.
The dtypes differ. Even once dispatch is fixed, the same payload reads back
differently depending on the reader's pandas version — an
objectcolumn becomesstr, and missing values becomenanrather thanNone. The reader's pandasversion decides what a task receives, not the writer's, so a mixed-version
deployment silently hands tasks data that differs from what was pushed.
Approach
pandas<3is declared everywhere Airflow declares pandas — 45 specifiers across14
pyproject.tomlfiles: thepandasextra oncommon-sql(whatapache-airflow[pandas]resolves to), the task-sdk development group, and thethirteen providers that pin pandas independently of
common-sql—amazon,apache/hdfs,apache/hive,databricks,exasol,google,papermill,postgres,presto,salesforce,snowflake,trinoandweaviate.Capping only
common-sqlwould have left a gap: installing any of those providerscould still resolve pandas 3, so the constraint would not hold and the user would
meet the runtime refusal instead of simply being kept on a supported version.
pandas-gbqinproviders/googleis deliberately not capped — a differentpackage, unaffected by this. Provider
README.rstfiles anduv.lockareregenerated to match.
That constraint cannot be relied on by itself. pandas is an optional
dependency: a deployment can install pandas 3 directly, or pull it in through
another package, and nothing in Airflow's metadata prevents it. So the constraint
is backed by a runtime check that fails with a message naming pandas, the version
found, and what to do.
The check needs the pandas 3 qualname registered to be reachable at all.
pandas.DataFrameis added to the registry only so that the request reaches thismodule and can be refused deliberately — not to support it. Without that entry the
guard is dead code, because serde never dispatches.
Reads are refused as well as writes. A payload written under pandas 2 comes
back with pandas 3 dtypes, so accepting it would give the task different data than
was pushed, with no signal at all. An error on both sides is the safer outcome, and
the write-side failure is the more recoverable of the two.
Behaviour change
With pandas >= 3 installed, pushing or pulling a
DataFramethrough XCom raises:Previously the same situation produced an opaque serde
TypeErroron write, and —once dispatch was corrected — silently different dtypes on read. Nothing changes for
pandas 2 users.
This defers pandas 3 support, it does not drop it. Moving from pandas 2 to
pandas 3 is a deliberate migration for users to make, given the dtype changes.
Airflow will enable it in a future release; see the follow-up PR linked in the
comments.
Test plan
task-sdk/tests/task_sdk/serde/test_serializers.py— 11 pandas cases passtest_pandas_3_dataframe_xcom_is_refused—3.0.0,3.0.5and4.1.2allrefused on both
serializeanddeserializetest_pandas_3_dataframe_name_is_registered— guards the reachabilityprecondition, so the guard cannot silently become dead code if the registry
entry is dropped
test_pandas_2_dataframe_xcom_still_round_trips—2.1.2and2.3.3unaffected
pyproject.tomlre-parsed withtomllibto confirm it is still validpandas-gbqconfirmed unchangeduv.lockregenerated (947 packages resolved) with no uncapped pandas entryremaining, and provider
README.rstfiles regenerated from the new pinsruff check/ruff formatcleanWas generative AI tooling used to co-author this PR?
Generated-by: Claude Opus 5 (1M context) following the guidelines at
https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions