I believe there is a bug with aware datetime when using timedelta due to DST

In Python version 3.12.3 (I checked 3.12.4),

I found the following bug:

x = datetime.datetime(2023, 10, 29, 2, 45, tzinfo=zoneinfo.ZoneInfo(‘CET’))
prints as: 2023-10-29 02:45:00+02:00

x + datetime.timedelta(minutes=15)
prints as: 2023-10-29 03:00:00+01:00 this should be instead 2023-10-29 02:00:00+01:00

Sorry in advance if I overlooked a clearly written passage in the datetime or zoneinfo documentation!

My first guess was maybe this was an issue with specifying “CET” instead of “Europe/Paris” (or equivalent). But after playing around with it, that doesn’t seem to be the case, and datetime.datetime(2023, 10, 29, 2, 45, tzinfo=zoneinfo.ZoneInfo('CET')).tzname() evaluates to CEST anyway.

The relevant piece of information looks like the fold attribute, but math with timedelta ignores the fold. The only way I can figure out to do this correctly is convert to UTC, do your math, and then convert back.

from datetime import datetime, timedelta, UTC
from zoneinfo import ZoneInfo

CET  = ZoneInfo('CET')
x = datetime(2023, 10, 29, 2, 45, tzinfo=CET)
y = (x.astimezone(UTC) + timedelta(minutes=15)).astimezone(CET)

x.fold  # 0
y.fold  # 1
x.isoformat()  # '2023-10-29T02:45:00+02:00'
y.isoformat()  # '2023-10-29T02:00:00+01:00'

PEP 495, which introduced the fold parameter, has this to say:

Users of pre-PEP implementations of tzinfo will not see any changes in the behavior of their aware datetime instances. Two such instances that differ only by the value of the fold attribute will not be distinguishable by any means other than an explicit access to the fold value. (This is because these pre-PEP implementations are not using the fold attribute.)

On the other hand, if an object’s tzinfo is set to a fold-aware implementation, then in a fold or gap the value of fold will affect the result of several methods: utcoffset(), dst(), tzname(), astimezone(), strftime() (if the “%Z” or “%z” directive is used in the format specification), isoformat(), and timetuple().

and also:

No new implementations of datetime.tzinfo abstract class are proposed in this PEP. The existing (fixed offset) timezones do not introduce ambiguous local times and their utcoffset() implementation will return the same constant value as they do now regardless of the value of fold.

So it seems like this is basically as intended, and a third-party implementation would be needed to support fold-aware math. I checked, and dateutil seems to have the same behavior for math using their relativedelta class.

1 Like

Thinking about it more, there would probably be unpleasantly surprising cases for a lot of people if timedelta math was fold-aware. Consider the case of adding some number of days to a datetime, say 4 weeks

x = datetime(2023, 10, 10, tzinfo=CET)
(x + timedelta(days=28)).isoformat()   # '2023-11-07T00:00:00+01:00'

# fold-aware
(x.astimezone(UTC) + timedelta(days=28)).astimezone(CET).isoformat()  # '2023-11-06T23:00:00+01:00'

if the range happens to include a change in DST, the fold-aware version will be at a different hour than your starting time, but this is rarely what someone wants for that kind of math.

1 Like

Yes, more or less. It’s worth noting that an aware datetime does NOT mean “this point in time, and oh, we’d like it displayed in this timezone please”. It means “this time, on this clock as defined by a timezone”. When you add 15 minutes to 02:45, you get 03:00, and this then has to be adjusted to match the clock shift for DST.

To express the concept of “what time will it be in fifteen minutes”, you need to work in UTC, converting to a time zone for display. UTC simply advances forward [1] so adding a timedelta to a UTC datetime should give you the time you’re looking for:

(x.astimezone(datetime.UTC) + datetime.timedelta(minutes=15)).astimezone(x.tzinfo)

In general, if you’re trying to work with instants in time, don’t have other timezones attached to them - just work with UTC. Convert to the timezone you want at the very end, for the display. If you’re keeping the datetimes in that timezone, it should be because you want to work with civil time; for example, you can express the concept “schedule this meeting at 9AM each weekday” without having to concern yourself with the fact that it might not be 86400 seconds between those meetings.


  1. let’s not get into relativity here ↩︎

1 Like

Even without relativity, UTC can jump backwards if a negative leap second were to be added, although that hasn’t happened yet. You need to use TAI if strictly monotonically increasing time is important to you.

Leap seconds are being abandoned. The odds that, in the next few years, a negative leap second will be introduced, are so close to zero as makes no difference.

Maybe not as unlikely as you’d think! This paper predicts a negative leap second by 2029, while leap seconds are scheduled to phase out by 2035.

Paywalled. Anyhow, in terms of the current discussion, it’s still far less messy than civil time.