diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index 8993049a720b1c..7505028b740b90 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -1329,9 +1329,11 @@ Supported operations: Naive and aware :class:`.datetime` objects are never equal. - If both comparands are aware, and have the same :attr:`!tzinfo` attribute, - the :attr:`!tzinfo` and :attr:`~.datetime.fold` attributes are ignored and - the base datetimes are compared. + If both comparands are aware, and have the same :attr:`!tzinfo` and + :attr:`~.datetime.fold` attributes, the base datetimes are compared. + If both comparands are aware, and have the same :attr:`!tzinfo` but + differing :attr:`~.datetime.fold` attributes, the objects are converted to + timestamps, and the timestamps are compared. If both comparands are aware and have different :attr:`~.datetime.tzinfo` attributes, the comparison acts as comparands were first converted to UTC datetimes except that the implementation never overflows. @@ -1345,9 +1347,11 @@ Supported operations: Order comparison between naive and aware :class:`.datetime` objects raises :exc:`TypeError`. - If both comparands are aware, and have the same :attr:`!tzinfo` attribute, - the :attr:`!tzinfo` and :attr:`~.datetime.fold` attributes are ignored and - the base datetimes are compared. + If both comparands are aware, and have the same :attr:`!tzinfo` and + :attr:`~.datetime.fold` attributes, the base datetimes are compared. + If both comparands are aware, and have the same :attr:`!tzinfo` but + differing :attr:`~.datetime.fold` attributes, the objects are converted to + timestamps, and the timestamps are compared. If both comparands are aware and have different :attr:`~.datetime.tzinfo` attributes, the comparison acts as comparands were first converted to UTC datetimes except that the implementation never overflows. @@ -1364,6 +1368,11 @@ Supported operations: The default behavior can be changed by overriding the special comparison methods in subclasses. +.. versionchanged:: 3.15 + Comparison between :class:`.datetime` objects with matching :attr:`!tzinfo` + and differing :attr:`~.datetime.fold` attributes uses timestamps for + comparison, so that ordering is preserved even in the case of a repeated + interval. Instance methods: diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index b6d68f2372850a..ef2160f0c41bc4 100644 --- a/Lib/_pydatetime.py +++ b/Lib/_pydatetime.py @@ -2296,6 +2296,11 @@ def _cmp(self, other, allow_mixed=False): myoff = otoff = None if mytz is ottz: + # If the objects' fold properties differ, the `fold=1` timestamp may + # follow the `fold=0` timestamp even though fielf-by-field comparison + # would otherwise conclude that it occurs before. (#146236) + if self.fold != other.fold: + return _cmp(self.timestamp(), other.timestamp()) base_compare = True else: myoff = self.utcoffset() diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index e264433ca590bf..dce629e332c6d8 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -15,6 +15,7 @@ import textwrap import unittest import warnings +import zoneinfo from array import array @@ -5985,6 +5986,20 @@ def test_tricky(self): self.assertEqual(astz.replace(tzinfo=None), expected) asutcbase += HOUR + @unittest.skipUnless("America/Los_Angeles" in zoneinfo.available_timezones(), + "Can't find timezone database") + def test_ordering_dst(self): + for utc in utc_real, utc_fake: + for tz in zoneinfo.ZoneInfo("America/Los_Angeles"), zoneinfo.ZoneInfo("America/New_York"): + print(f"{tz!r} {self.dstoff!r} {utc is utc_fake} {id(datetime)}") + tm = tm0 = self.dstoff.replace(tzinfo=tz, hour=0) + print(f"{tm0!r}") + for h in range(4): + for m in 1, 30, 59: + tm1 = (tm.astimezone(utc) + timedelta(hours=h, minutes=m)).astimezone(tz) + print(f"{tm1!r}") + self.assertLess(tm0, tm1) + tm0 = tm1 def test_bogus_dst(self): class ok(tzinfo): diff --git a/Lib/test/test_zoneinfo/test_zoneinfo.py b/Lib/test/test_zoneinfo/test_zoneinfo.py index aaab4709464fd0..d550063baaf748 100644 --- a/Lib/test/test_zoneinfo/test_zoneinfo.py +++ b/Lib/test/test_zoneinfo/test_zoneinfo.py @@ -389,6 +389,21 @@ def test_folds_from_utc(self): dt_after = dt_after_utc.astimezone(zi) self.assertEqual(dt_after.fold, 1, (dt_after, dt_utc)) + + def test_ordering_dst(self): + UTC = self.klass("UTC") + dstoff = datetime(2002, 10, 27, 1) + tz = self.klass("America/Los_Angeles") + print(f"{tz!r} {dstoff!r} {id(datetime)}") + tm = tm0 = dstoff.replace(tzinfo=tz, hour=0) + print(f"{tm0!r}") + for h in range(4): + for m in 1, 30, 59: + tm1 = (tm.astimezone(UTC) + timedelta(hours=h, minutes=m)).astimezone(tz) + print(f"{tm1!r}") + self.assertLess(tm0, tm1) + tm0 = tm1 + def test_time_variable_offset(self): # self.zones() only ever returns variable-offset zones for key in self.zones(): diff --git a/Misc/NEWS.d/next/Library/2026-03-21-10-14-35.gh-issue-146236.BB_ShV.rst b/Misc/NEWS.d/next/Library/2026-03-21-10-14-35.gh-issue-146236.BB_ShV.rst new file mode 100644 index 00000000000000..6e8ca3e176adf1 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-03-21-10-14-35.gh-issue-146236.BB_ShV.rst @@ -0,0 +1,3 @@ +Comparison of datetime values with ``fold=1`` now compares the objects' +timestamps so that correct ordering of timestamps is maintained at the end +of DST. diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 9d803dc94b64c7..b88dfb1bda11bf 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -114,6 +114,9 @@ typedef struct { #define CONST_EPOCH(st) st->epoch #define CONST_UTC(st) ((PyObject *)&utc_timezone) +static PyObject * +datetime_timestamp(PyObject *op, PyObject *Py_UNUSED(dummy)); + static datetime_state * get_module_state(PyObject *module) { @@ -6565,6 +6568,18 @@ datetime_richcompare(PyObject *self, PyObject *other, int op) } if (GET_DT_TZINFO(self) == GET_DT_TZINFO(other)) { + // If the objects' fold properties differ, the `fold=1` timestamp may + // follow the `fold=0` timestamp even though fielf-by-field comparison + // would otherwise conclude that it occurs before. (#146236) + if (DATE_GET_FOLD(self) != DATE_GET_FOLD(other)) { + PyObject *ts_self = datetime_timestamp(self, NULL); + PyObject *ts_other = datetime_timestamp(other, NULL); + PyObject *result = PyObject_RichCompare(ts_self, ts_other, op); + Py_DECREF(ts_self); + Py_DECREF(ts_other); + return result; + } + diff = memcmp(((PyDateTime_DateTime *)self)->data, ((PyDateTime_DateTime *)other)->data, _PyDateTime_DATETIME_DATASIZE);