Skip to content

changed localtime to gmtime to fix daylight savings issues - #3805

Closed
JamesUnicomb wants to merge 2 commits into
pybind:masterfrom
JamesUnicomb:localtime_to_gmtime
Closed

JamesUnicomb wants to merge 2 commits into
pybind:masterfrom
JamesUnicomb:localtime_to_gmtime

Conversation

@JamesUnicomb

Copy link
Copy Markdown

I was having issues with pybind using UTC times properly. As well, daylight savings datetimes were failing "roundtrip" tests.

Linked to this discussion:
#3451 (reply in thread)

Comment thread include/pybind11/chrono.h
inline std::tm *localtime_thread_safe(const std::time_t *time, std::tm *buf) {
inline std::tm *gmtime_thread_safe(const std::time_t *time, std::tm *buf) {
#if (defined(__STDC_LIB_EXT1__) && defined(__STDC_WANT_LIB_EXT1__)) || defined(_MSC_VER)
if (localtime_s(buf, time))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the Windows tests are failing. Do you already have a plan for that? Could we simply remove the #if and use the same code, with mutex, everywhere?

Comment thread include/pybind11/chrono.h
localtime.tm_hour,
localtime.tm_min,
localtime.tm_sec,
return PyDateTime_FromDateAndTime(gmtime.tm_year + 1900,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this change the behavior?
Before: local timezone
After: actually UTC but (incorrectly?) appearing as local timezone?

Same question from a different angle: as-is this PR fixes the roundtrip issue, i.e. fixes an internal consistency issue, but is it still externally inconsistent?

Do we have to tell PyDateTime_FromDateAndTime() somehow that the passed time is UTC?

I just looked a bit, seeing this warning 3 times under https://docs.python.org/3/library/datetime.html:

Warning Because naive datetime objects are treated by many datetime methods as local times, it is preferred to use aware datetimes to represent times in UTC. As such, the recommended way to create an object representing a specific timestamp in UTC is by calling datetime.fromtimestamp(timestamp, tz=timezone.utc).

How can handle this correctly from the C API?

Comment thread include/pybind11/chrono.h
inline std::tm *localtime_thread_safe(const std::time_t *time, std::tm *buf) {
inline std::tm *gmtime_thread_safe(const std::time_t *time, std::tm *buf) {
#if (defined(__STDC_LIB_EXT1__) && defined(__STDC_WANT_LIB_EXT1__)) || defined(_MSC_VER)
if (localtime_s(buf, time))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function needs to be swapped out too to stop the tests from failing

Comment thread include/pybind11/chrono.h
localtime.tm_hour,
localtime.tm_min,
localtime.tm_sec,
return PyDateTime_FromDateAndTime(gmtime.tm_year + 1900,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JamesUnicomb I am not sure about the UTC issue, but the datetime roundtrip issue can be easily resolved. We just need to use PyDateTime_FromDateAndTimeAndFold instead of PyDateTime_FromDateAndTime and make sure to properly set the is_fold argument.

Comment thread include/pybind11/chrono.h
}

value = time_point_cast<Duration>(system_clock::from_time_t(std::mktime(&cal)) + msecs);
value = time_point_cast<Duration>(system_clock::from_time_t(timegm(&cal)) + msecs);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

timegm isn't available on windows, so this line needs to change to something like:

#if defined(_MSC_VER)
    time_t tt = _mkgmtime(&cal);
#else
    time_t tt = timegm(&cal);
#endif

    value = time_point_cast<Duration>(system_clock::from_time_t(tt) + msecs);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As @Skylion007 pointed out before, we don't have to go to UTC to solve the roundtrip problem: #3805 (comment)
I think it's better to keep using the local time zone, but make proper use of the fold feature.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with that solution would std::system_clock time points go into python as time aware (UTC) datetime objects, and python datetime objects go into C++ as UTC?

C++ system_clock is Unix Time (so UTC modulo leap seconds and such), so the time zone conversions are a little surprising.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe python datetime objects should be in local time in both? I am not sure we are actually attaching the time zone to the Python objects (we should but it doesn't seem straight forward using the raw C - API).

If it's too tricky, we might want to call the Python datetime function directly if we need to.

@voznesenskym

Copy link
Copy Markdown
Contributor

I'm a noob here, so please, disregard readily and with prejudice if what I write does not jive with the spirit of the project.

It feels like localtime_thread_safe is a critical part of the contract for this cast, and that theres probably upstream customer assumptions around it, esp if its not a 1:1 conversion (as you said, daylight savings time can cause off-by-some-amount-of-time discrepancies).

It feels, then, like the right thing to do here would be make this a perfectly backward compatible change, via pre-processor macro, something like:

PYBIND11_USE_LEGACY_LOCAL_TIME (where the default is now the gmtime) (my preference)

or

PYBIND11_USE_GMTIME (where the default is the old behavior)

@rwgk

rwgk commented Oct 10, 2026

Copy link
Copy Markdown
Collaborator

I remembered this old issue and used codex to dig it up and analyze:

codex GPT-6.1-Sol ultra

The likely match is PR #3805, “changed localtime to gmtime to fix daylight savings issues”, opened in March 2022. It is still open and unmerged. The related issue #1638 is closed, although later comments report continuing problems.

A real fall-back bug remains today. I checked current upstream master (6254086d…) and the latest release, v3.1.0; their chrono converters are identical:

For Los Angeles’s upcoming November 1 transition, I reproduced:

Conversion Expected Current result
Difference between 01:30, fold=0 and 01:30, fold=1 One hour Zero
Return the second occurrence of 01:30 from C++ fold=1 fold=0, timestamp one hour early

The original report needs a distinction: its Sydney example was 2021-10-03 02:18, inside the skipped spring-forward hour. That local time never existed. Python itself normalizes it to 03:18 through timestamp()/fromtimestamp(). Preserving the original wall time through an actual instant is impossible under local-time semantics. PEP 495 specifies this behavior.

The old PR proposed switching all conversions to UTC. Review identified Windows failures and a change to the documented local-time behavior. Your review favored retaining local time and correctly supporting fold. That remains the direction I recommend.

The fix should:

  1. Honor input fold and determine output fold, using Python’s local timestamp()/fromtimestamp() semantics.
  2. Keep whole seconds and microseconds separate to preserve precision.
  3. Add fixed-date tests that explicitly check timestamps and fold, including spring gaps and a 30-minute overlap. Ordinary datetime equality ignores fold, so it misses this bug.

Both regular and free-threaded CPython 3.14.4 passed all 43 existing chrono tests, while the new standalone probe found 16 incorrect conversions in each environment. This was focused chrono validation; Catch2 and the full suite were not run. The reproducer, regular results, and free-threaded results are available locally. The source fix is still outstanding.

I’d target a release before October 25, 2026, when EU clocks go back; US/Pacific follows on November 1. Downstream extensions will need rebuilding with the fixed headers. EU schedule, NIST US schedule.

@rwgk

rwgk commented Oct 19, 2026

Copy link
Copy Markdown
Collaborator

Closing after the alternative PR #6202 was merged.

@rwgk rwgk closed this Oct 19, 2026
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.

5 participants