Timezone bugs come from mixing times that do not say which zone they are in, and from assuming a day always has 24 hours. The cure is to be explicit about timezones everywhere, and to work in UTC internally.
Naive and aware datetimes
A naive datetime has no zone, so datetime(2025, 3, 1, 9, 0) could mean any place on earth. An aware datetime carries its zone or offset. Python refuses to compare or subtract a naive and an aware value (it raises an error), which is a helpful warning, but code often gets around it by stripping the zone, and that causes the real bug.
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
utc_now = datetime.now(timezone.utc)
ist = utc_now.astimezone(ZoneInfo("Asia/Kolkata"))zoneinfo is in the standard library since Python 3.9, and it uses the IANA names such as Asia/Kolkata and America/New_York. Use names, not fixed offsets like +05:30, because names know the daylight saving rules.
The rules that prevent most problems
- Store timestamps in UTC, and compute in UTC.
- Convert to a local zone only at the edge, for display or for working out a business date.
- Parse ISO 8601 strings that include the offset (
2025-03-01T10:00:00+05:30), and reject or flag strings without one. - Do not use
datetime.utcnow(), which returns a naive value and is deprecated in recent Python versions. Usedatetime.now(timezone.utc).
Daylight saving time
In zones with DST, one day a year has 23 hours, and another has 25. A local time in the spring gap (2:30 a.m. when clocks jump from 2:00 to 3:00) does not exist, and a local time in the autumn overlap happens twice. Code that adds timedelta(days=1) to a local time, or assumes midnight to midnight is 24 hours, will be wrong on those days. Doing the arithmetic in UTC avoids it.
Partitioning by day
Decide which zone defines the day for each daily table, and write it down. A table partitioned by UTC date and a business report that uses India time will disagree around midnight, which is a classic source of "daily numbers are off". Keep both a UTC timestamp and a business date column.