Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

surprising behaviour of ZoneInfo and datetime and invalid points in time #116038

Closed
cjw296 opened this issue Feb 28, 2024 · 2 comments
Closed

surprising behaviour of ZoneInfo and datetime and invalid points in time #116038

cjw296 opened this issue Feb 28, 2024 · 2 comments
Labels
type-bug An unexpected behavior, bug, or error

Comments

@cjw296
Copy link
Contributor

cjw296 commented Feb 28, 2024

Bug report

Bug description:

The standard libary's ZoneInfo and datetime implementations allow you to create invalid objects, such as this one, which lands in the "non-existent" DST jump forward in the UK, early 2024:

>>> tz = ZoneInfo(key='Europe/London')
>>> datetime(2024, 3, 31, 1, 30, tzinfo=tz)
datetime.datetime(2024, 3, 31, 1, 30, tzinfo=zoneinfo.ZoneInfo(key='Europe/London'))

This feels like it should raise an exception, given that it is not a valid point in time.

datetime.astimezone has pretty confusing behaviour over the same non-existent point in time:

>>> datetime(2024, 3, 31, 0, 59).astimezone(tz)
datetime.datetime(2024, 3, 31, 0, 59, tzinfo=zoneinfo.ZoneInfo(key='Europe/London'))

The above is fine, but these two result in datetime objects for another point in time rather than the invalid parameters they were created with:

>>> datetime(2024, 3, 31, 1, 0).astimezone(tz)
datetime.datetime(2024, 3, 31, 0, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/London'))
>>> datetime(2024, 3, 31, 1, 30).astimezone(tz)
datetime.datetime(2024, 3, 31, 0, 30, tzinfo=zoneinfo.ZoneInfo(key='Europe/London'))
>>> datetime(2024, 3, 31, 1, 59).astimezone(tz)
datetime.datetime(2024, 3, 31, 0, 59, tzinfo=zoneinfo.ZoneInfo(key='Europe/London'))

After the transition, things are once again correct

>>> datetime(2024, 3, 31, 2, 0).astimezone(tz)
datetime.datetime(2024, 3, 31, 2, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/London'))

CPython versions tested on:

3.12

Operating systems tested on:

No response

@tim-one
Copy link
Member

tim-one commented Mar 9, 2024

Yes - as always, operations within a time zone use naïve time. As always, if you want time zone to matter, you need to convert between time zones (typically to UTC, do computation there, and then convert back).

For astimezone() specifically, the docs warn:

If self.tzinfo is tz, self.astimezone(tz) is equal to self: no adjustment of date or time data is performed.

In any case of ambiguous or missing times around a transition, fold determines the outcome of conversion. For fold=0 (the default), the rules in effect before the transition are used, and if fold=1 then the rules in effect after the transition are used.

>>> fold0 = datetime(2024, 3, 31, 1, 30, tzinfo=tz)
>>> fold1 = fold0.replace(fold=1)
>>> print(fold0) # note UTC offset 0: before DST started
2024-03-31 01:30:00+00:00
>>> fold0.utcoffset()
datetime.timedelta(0)
>>> print(fold0.astimezone(utc))
2024-03-31 01:30:00+00:00

>>> print(fold1) # note UTC offset of an hour: after DST started
2024-03-31 01:30:00+01:00
>>> fold1.utcoffset()
datetime.timedelta(seconds=3600)
>>> print(fold1.astimezone(utc))
2024-03-31 00:30:00+00:00

# So the UTC times differ by an hour, as expected.
# Converting back gives legit wall-clock times:
>>> print(fold0.astimezone(utc).astimezone(tz))
2024-03-31 02:30:00+01:00
>>> print(fold1.astimezone(utc).astimezone(tz))
2024-03-31 00:30:00+00:00

# Note that DST may remain surprising: despite that DST shifted
# the local clock by an hour, the two resulting local times
# differ by _two_ hours on the local clock. A consequence of
# that there are no times of the form 1:MM:SS on the local clock.

This feels like it should raise an exception, given that it is not a valid point in time.

See PEP 495 for why nothing ever complains about missing or ambiguous fold/gap times. It gives simple code you can use to do whatever you want in such cases. But, to date, I have yet to hear of anyone who cared enough to bother 😉.

All that said, I'm going to close this as "not planned". Since everything here is working as intended, and any change would be a backward-compatibility nightmare, it would require a new PEP to argue the case at exhausting length.

@tim-one
Copy link
Member

tim-one commented Mar 9, 2024

Oops - clicked a wrong button.

@tim-one tim-one closed this as completed Mar 9, 2024
@erlend-aasland erlend-aasland closed this as not planned Won't fix, can't repro, duplicate, stale Mar 10, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type-bug An unexpected behavior, bug, or error
Projects
Archived in project
Development

No branches or pull requests

3 participants