Q&A library Python

Why is adding a day to a datetime wrong across a daylight saving change?

Python Advanced datetimedsttimezone

Short answer

Adding a timedelta to an aware datetime keeps the wall clock time, so across a clock change only 23 or 25 hours actually pass. Convert to UTC, add the duration, then convert back if you mean a true elapsed day.

The short version

Adding timedelta(days=1) to a timezone-aware datetime advances the wall clock by a day, not necessarily by 24 hours. Across a daylight saving change, only 23 or 25 hours really elapse. If you mean a true 24-hour duration, convert to UTC, add, and convert back.

Seeing the difference

The UK clocks went forward on 28 March 2021.

from datetime import datetime, timedelta
from zoneinfo import ZoneInfo

london = ZoneInfo("Europe/London")
start = datetime(2021, 3, 27, 12, tzinfo=london)

wall = start + timedelta(days=1)
print(wall)   # 2021-03-28 12:00:00+01:00

The result is noon the next day, as you would expect on a calendar. But only 23 hours actually passed, because an hour vanished overnight.

utc = ZoneInfo("UTC")
elapsed = (start.astimezone(utc) + timedelta(days=1)).astimezone(london)
print(elapsed)   # 2021-03-28 13:00:00+01:00

Adding 24 real hours lands on 13:00, not 12:00. Both answers are correct; they answer different questions.

Which one do you want

Ask what "a day later" means in your context.

If you mean the same clock time tomorrow (a meeting at noon, a daily report at 9am), the first behaviour is right. People expect a 9am meeting to stay at 9am through a clock change.

If you mean 24 hours of elapsed time (a timer, a rate calculation, a service level agreement measured in hours), the second is right, and the naive addition silently gives you an hour too few.

Why this causes real bugs

The bug is invisible for most of the year and appears twice annually. A duration computed by subtracting wall-clock datetimes across a clock change is off by an hour, which quietly corrupts any hourly rate, any elapsed-time metric, and any billing based on duration. Because it only bites twice a year, it usually reaches production.

The safe discipline

Store and compute in UTC, and convert to a local zone only for display.

now_utc = datetime.now(ZoneInfo("UTC"))
later = now_utc + timedelta(hours=24)          # unambiguous, no clock changes
display = later.astimezone(london)              # only for showing a person

UTC has no daylight saving, so arithmetic in it is always what it appears to be. This single habit removes the whole class of problems.

Times that do not exist or happen twice

When clocks go forward, local times in the skipped hour never occur. When they go back, times in the repeated hour occur twice, and a local timestamp alone cannot say which. This is why a local wall-clock time is not a reliable identifier for an instant, and why logs and events should be stamped in UTC.

The rule

Never do duration arithmetic in a local timezone. Convert to UTC, do the maths, convert back for display. Keep wall-clock addition for calendar-style scheduling, where staying at the same clock time is the point.

Need this solved on your own data?

The library is free and always will be. When the problem is bigger than an answer, we take on the work.

Start a project