Skip to content

[SPARK-59467][PYTHON] Normalize ArrowDtype timestamps to the session timezone in pandas conversion - #58765

Open
fangchenli wants to merge 6 commits into
apache:masterfrom
fangchenli:arrowdtype-timestamp-session-tz
Open

fangchenli wants to merge 6 commits into
apache:masterfrom
fangchenli:arrowdtype-timestamp-session-tz

Conversation

@fangchenli

@fangchenli fangchenli commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

Add a pd.ArrowDtype branch to _check_series_convert_timestamps_internal so timezone-naive ArrowDtype timestamp columns are localized to the session timezone like numpy datetime64 columns. Localization uses pyarrow.compute.assume_timezone with ambiguous="latest", matching the standard-time choice of the numpy branch, and falls back to the numpy branch for zone ids pyarrow cannot parse (e.g. UTC+01:00). Timezone-aware ArrowDtype columns are converted to UTC and passed through.

This is a precursor to #54518.

Why are the changes needed?

The function only recognized numpy datetime64 and pd.DatetimeTZDtype. A naive timestamp[us][pyarrow] column fell through unchanged and was interpreted as UTC, so in a non-UTC session values were shifted by the session's UTC offset in createDataFrame from pandas and in pandas UDF results. This contradicts the documented behavior of spark.sql.session.timeZone.

Does this PR introduce any user-facing change?

Yes. Timezone-naive ArrowDtype timestamp columns are now interpreted in the session timezone instead of UTC. To keep the previous interpretation, make the column timezone-aware in UTC. A migration guide entry is added.

How was this patch tested?

Unittests added.

Was this patch authored or co-authored using generative AI tooling?

Yes, co-authored with Claude Code Opus 5.

fangchenli and others added 6 commits September 12, 2026 21:42
…andas conversion

_check_series_convert_timestamps_internal only handled numpy datetime64 and
DatetimeTZDtype, so a timezone-naive pd.ArrowDtype timestamp column fell
through unnormalized and was interpreted as UTC instead of the session
timezone. This affected createDataFrame from pandas and pandas UDF output in
non-UTC sessions.

Add an ArrowDtype branch that localizes with pc.assume_timezone using
ambiguous="latest", matching the standard-time choice of the numpy branch,
and falls back to the numpy branch for timezone=None and for zone ids
pyarrow cannot parse (e.g. "UTC+01:00").

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MNnkeZfwvDybyqry1C3rj2
- Move the migration note to pyspark_upgrade.rst, where pandas conversion
  notes live, and qualify it for Spark Connect createDataFrame without a
  schema, which does not infer ArrowDtype columns as TimestampType.
- Collapse the unreachable timezone=None sub-branch into the ArrowInvalid
  fallback and document that nonexistent times take the fallback too.
- Route the createDataFrame-level test through create_arrow_array_from_pandas,
  the path createDataFrame actually uses, and assert timezone=None on values
  rather than an environment-dependent dtype string.
- Fix import ordering flagged by ruff I001; drop a stale nested-timestamp TODO.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MNnkeZfwvDybyqry1C3rj2
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MNnkeZfwvDybyqry1C3rj2
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MNnkeZfwvDybyqry1C3rj2
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MNnkeZfwvDybyqry1C3rj2
@HyukjinKwon

Copy link
Copy Markdown
Member

cc @gaogaotiantian

@gaogaotiantian

Copy link
Copy Markdown
Contributor
  1. There's a mypy failure - f"datetime64[{unit}]" is not guaranteed to be valid (even though logically it probably is). We can probably ignore here, or we can check unit and assign literals to this argument.
  2. Is ArrowDtype timestamp commonly used now? This is a breaking change I think. I mean our timestamp localization is a mess for naive timestamps. But maybe this is not too bad.
  3. Do we care about us vs ns thing? It bothers me a bit - the unit is different but the result is the same - is it intentional?
  4. We used the str interpolation for the result comparision - self.assertEqual(str(arrow_out.iloc[0]), str(numpy_out.iloc[0])) why is that?

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.

3 participants