Time Zones
Date and Time in Practice
Chapter 4 ยท Time Zones
Chapter 3 used fixed offsets such as +05:30. A fixed offset is the right tool when the offset really is fixed, but most places change theirs during the year. This chapter introduces real time zones through the standard-library zoneinfo module, which looks zones up by name in the IANA time zone database. The examples use Python 3.14, and every snippet was run; the output shown in the comments is what Python printed.
A Zone Is a Set of Rules
A ZoneInfo object is named by an IANA key of the form Region/City, such as Europe/London or Asia/Tokyo. It does not hold one offset; it holds the history and rules for a place, and answers with the offset that applies to the date you give it.
The same zone gave +01:00 in July and +00:00 in January. A fixed timezone object could never do that. The abbreviation (BST, GMT) is also a result, not an input: tzname() reports it, but you never construct a zone from one.
timezone(timedelta(...)) | ZoneInfo("Region/City") | |
|---|---|---|
| Offset | one value, always | depends on the date |
| Knows seasonal changes? | no | yes |
| Source of data | you | the IANA database |
| Best for | an offset that was recorded as a number | a place that people live in |
Converting Between Zones
Conversion is the astimezone you met in Chapter 3. The moment stays the same; only the reading and label change.
The last line is the point of the exercise: three different readings, one instant, and equality holds. The offsets are not always whole hours (India is +05:30, Nepal +05:45), which is another reason never to compute a conversion by adding hours yourself.
datetime(..., tzinfo=ZoneInfo(...)) is correct when you know the reading was made in that zone. It is the same idea as replace(tzinfo=...) from Chapter 3: it labels, it does not convert. Chapter 5 returns to the readings where even this labelling is ambiguous.
When a Name Is Not Found
Keys must match the database exactly. Abbreviations and misspellings are rejected with ZoneInfoNotFoundError, so user-supplied zone names should always be validated:
There is a second way to get the same error, and it has nothing to do with spelling. zoneinfo reads the zone database from the operating system. Windows does not ship one, so on a Windows machine every lookup fails until you install the tzdata package, which is a pure data package from the Python Package Index:
If a program works on one machine and raises ZoneInfoNotFoundError for a perfectly good key on another, suspect a missing database before you suspect the key. Listing available_timezones() (from zoneinfo) shows what the current machine can resolve.
Store UTC, Show Local
The usual design is to convert to UTC as soon as a moment enters the program, store and compare it in UTC, and convert to a viewer's zone only to display it. UTC has no seasonal changes, so UTC values never become ambiguous, and sorting a column of them is correct.
Notice that two of the four readings fall on the next calendar day. That is normal, and it is why a stored date without its zone is not enough information to show a user.
Why Not Just Store the Offset?
A fixed offset recorded today describes today. A zone name describes a place, and the place's rules decide the offset on any other day. Compare a fixed -5 with America/New_York on a July date:
The fixed offset is wrong by an hour for the whole summer. If a user says "every Monday at 09:00 in New York", the thing to keep is the pair (09:00, America/New_York), not an offset.
Two aware values compare by instant, so zone choice does not affect equality when the instant is the same, and a shared clock reading in different zones is a different instant:
Challenges
Write world_clock(moment, names) that takes one aware UTC datetime and a list of zone names, and prints one line per zone with the local date and time, the abbreviation and the offset in hours. Use the moment 23:30 UTC on 20 November 2026 with Europe/London, Asia/Kolkata, Pacific/Auckland and America/Los_Angeles. Before you run it, predict which zones will show a different calendar date from UTC.
For Europe/London, America/New_York, Australia/Sydney, Asia/Kolkata and Asia/Tokyo, print the UTC offset in hours at noon on 15 January 2026 and at noon on 15 July 2026, and label each zone changes or constant. Predict the labels first, and say which zone has the larger offset in January.
Write get_zone(name) that returns a ZoneInfo, or None when the name is not found, and test it with Europe/Paris, BST, Europe/Londn and America/Chicago. Then take 18:00 on 3 September 2026 in Europe/Paris, convert it to UTC for storage, print the stored ISO text, show it in Europe/Paris, America/Chicago and Asia/Tokyo, and print whether the stored value equals the original.
Quick Reference
- Zone:
ZoneInfo("Region/City"), an IANA key; holds rules, not one offset - Convert:
dt.astimezone(ZoneInfo(...)); same instant, new reading - Label a known local reading:
datetime(..., tzinfo=ZoneInfo(...)) - Bad key:
ZoneInfoNotFoundError; abbreviations are not keys - Missing database (common on Windows):
py -m pip install tzdata - Store UTC; convert to a viewer's zone only to display
- Recurring local plans: keep the local time and the zone name, not an offset