Skip to content

Fix incorrect DateTime ordering during DST fold transitions#985

Open
Uttkarsh700 wants to merge 5 commits into
python-pendulum:masterfrom
Uttkarsh700:fix-dst-fold-comparison
Open

Fix incorrect DateTime ordering during DST fold transitions#985
Uttkarsh700 wants to merge 5 commits into
python-pendulum:masterfrom
Uttkarsh700:fix-dst-fold-comparison

Conversation

@Uttkarsh700

Copy link
Copy Markdown

Summary

Fix incorrect ordering comparisons for DateTime instances during DST fold transitions.

During the repeated hour after a daylight saving time (DST) rollback, two datetimes can represent different UTC instants while sharing the same timezone object. In this situation, ordering comparisons (<, <=, >, >=) can return results that do not reflect the actual chronological order.

This change normalizes both operands to UTC before performing ordering comparisons, ensuring that comparisons are based on the represented instant rather than the ambiguous local wall-clock time.


Problem

The issue can be reproduced with datetimes created during the DST fall-back transition:

d1 = pendulum.datetime(
    2023, 11, 5, 1, 15,
    tz="America/Los_Angeles",
    fold=1,
)

d2 = pendulum.datetime(
    2023, 11, 5, 1, 25,
    tz="America/Los_Angeles",
    fold=0,
)

print(d1.timestamp() > d2.timestamp())  # True
print(d1 < d2)                          # True (incorrect)

Although d1 represents a later UTC instant than d2, the ordering comparison reports the opposite result.

After reproducing the issue locally, I verified that the problem only affects relational ordering operators. Equality behavior remains unchanged.


Solution

The ordering operators now normalize both operands to UTC before performing the comparison.

The implementation:

  • normalizes both operands to UTC using astimezone(),
  • preserves existing equality semantics,
  • continues to support comparisons with standard datetime.datetime instances,
  • returns NotImplemented for unsupported comparison types,
  • ensures ordering comparisons reflect the actual UTC instant represented by each object.

The change is intentionally limited to ordering comparisons and does not modify equality behavior.


Why this approach?

Pendulum's comparison documentation states that comparisons are performed in UTC. Normalizing both operands before ordering comparisons keeps the implementation aligned with that documented behavior while limiting the scope of the change to relational operators only.


Testing

Added a regression test covering the DST fold scenario:

  • test_less_than_with_fold

Validation performed locally:

  • tests/datetime/test_comparison.py37 passed
  • tests/datetime565 passed

This change is intentionally scoped to ordering comparisons and does not modify equality semantics.

Fixes #855

…ons to UTC via astimezone and add regression test for DST fold
Copilot AI review requested due to automatic review settings July 15, 2026 07:56

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Fixes incorrect relational ordering of pendulum.DateTime values during DST “fold” transitions by normalizing operands to UTC before performing <, <=, >, and >= comparisons (aligning behavior with the documented “compare in UTC” semantics).

Changes:

  • Add rich ordering operator overrides (__lt__, __le__, __gt__, __ge__) that normalize both operands to UTC prior to comparison.
  • Add a regression test covering the DST fold ordering scenario.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
src/pendulum/datetime.py Implements UTC-normalized rich ordering operations intended to fix fold-related mis-ordering.
tests/datetime/test_comparison.py Adds a regression test for fold-related < ordering behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/pendulum/datetime.py
Comment on lines +557 to +577
# Rich ordering operations
def __lt__(self, other: datetime.datetime) -> bool: # type: ignore[override]
if not isinstance(other, datetime.datetime):
return NotImplemented

# Normalize both operands to UTC stdlib datetimes using base implementation
self_utc = datetime.datetime.astimezone(self, UTC)
other_dt = other if isinstance(other, DateTime) else DateTime.instance(other)
other_utc = datetime.datetime.astimezone(other_dt, UTC)

return datetime.datetime.__lt__(self_utc, other_utc)

def __le__(self, other: datetime.datetime) -> bool: # type: ignore[override]
if not isinstance(other, datetime.datetime):
return NotImplemented

self_utc = datetime.datetime.astimezone(self, UTC)
other_dt = other if isinstance(other, DateTime) else DateTime.instance(other)
other_utc = datetime.datetime.astimezone(other_dt, UTC)

return datetime.datetime.__le__(self_utc, other_utc)
Comment thread src/pendulum/datetime.py
Comment on lines +579 to +597
def __gt__(self, other: datetime.datetime) -> bool: # type: ignore[override]
if not isinstance(other, datetime.datetime):
return NotImplemented

self_utc = datetime.datetime.astimezone(self, UTC)
other_dt = other if isinstance(other, DateTime) else DateTime.instance(other)
other_utc = datetime.datetime.astimezone(other_dt, UTC)

return datetime.datetime.__gt__(self_utc, other_utc)

def __ge__(self, other: datetime.datetime) -> bool: # type: ignore[override]
if not isinstance(other, datetime.datetime):
return NotImplemented

self_utc = datetime.datetime.astimezone(self, UTC)
other_dt = other if isinstance(other, DateTime) else DateTime.instance(other)
other_utc = datetime.datetime.astimezone(other_dt, UTC)

return datetime.datetime.__ge__(self_utc, other_utc)
Comment on lines +418 to +419
assert d1.timestamp() > d2.timestamp()
assert not (d1 < d2)
@Uttkarsh700 Uttkarsh700 force-pushed the fix-dst-fold-comparison branch 2 times, most recently from 4ba4509 to 4ecded1 Compare July 15, 2026 08:52
@Uttkarsh700 Uttkarsh700 force-pushed the fix-dst-fold-comparison branch from 4ecded1 to 6f3a680 Compare July 15, 2026 09:12
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.

Comparing DateTimes before- and after-the-fold returns incorrect result

2 participants