Convert Aware Datetimes with zoneinfo
Convert an aware UTC instant to a named local zone and observe daylight saving changes.
Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons
Video lesson: Convert Aware Datetimes with zoneinfo
The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube
Jump to a chapter
Read the video transcript
Select a timestamp to watch that moment on YouTube.
- 00:00
Imagine a report created at twelve noon UTC. A colleague in New York wants the local time. If we subtract five hours, January's answer is seven in the morning. Is July's answer also seven? No. New York commonly uses a different offset during daylight saving time, so July noon UTC displays as eight in the morning. Today we will run the fixed-offset mistake, inspect the difference between naive and aware datetimes, use zoneinfo.ZoneInfo with the named America/New_York zone, and verify both seasons. The companion lesson has a browser console, worked examples, a checked task, mistakes, and a quiz.
- 00:42
There is one environmental requirement: named zones need an IANA timezone database. Local Python may use system data; another runtime, including browser Python, may need the tzdata package. We must test the actual runtime instead of assuming the database is present. Our examples begin from UTC timestamps with an explicit +00:00 offset. That makes each input a specific instant. We will not try to guess what an unlabeled local wall-clock time meant during a repeated autumn hour or a missing spring hour. A well-defined instant makes conversion straightforward.
- 01:23
The two printed hours are both seven because we subtracted the same five hours in both cases. The January result happens to match New York's winter offset. The July result does not match its daylight saving offset. The code is not raising an exception; it is confidently giving a wrong answer for one season. Another subtle problem is that subtracting a timedelta from an aware UTC datetime does not change its tzinfo. The result still carries a UTC offset even though the clock reading was shifted, so it mislabels the resulting instant. That is why manual arithmetic is not a safe way to convert timezones.
- 02:04
We need a timezone conversion, not a change to the instant. A fixed UTC minus five timezone can represent a fixed-offset rule, but it cannot encode New York's seasonal history. ZoneInfo uses a named IANA zone with date-specific rules. Before applying it, inspect the input itself. Does the timestamp carry an offset? Python calls datetimes without timezone context naive and those with timezone context aware. Converting a naive value can depend on the host machine's local timezone, which would make our lesson's output vary across environments. The first timestamp has no timezone information.
- 02:45
The second has an explicit zero offset, identifying a UTC instant. datetime.fromisoformat preserves that distinction; it does not magically know what an unlabeled timestamp means. For conversion from UTC, require an aware value. Checking both tzinfo and utcoffset is a clear guard in our exercise. Once the source instant is known, astimezone can calculate a local representation. ZoneInfo with America/New_York carries rules that choose the appropriate offset for the date. In winter the zone reports EST at minus five hours. In summer it reports EDT at minus four.
- 03:26
Those labels are useful for display, but the underlying offset and aware instant are what make the calculation precise. Be careful with a naive datetime and astimezone: Python may assume the machine's local timezone, so the same script can print different results on different hosts. Do not merely attach a zone label after subtracting hours; that can create a misleading aware object. The next function parses an explicitly offset ISO string, rejects a naive one, converts it with ZoneInfo, and returns an observable local hour and offset in seconds. The winter result is hour seven with an offset of minus eighteen thousand seconds, which is minus five hours.
- 04:11
The summer result is hour eight with minus fourteen thousand four hundred seconds, or minus four hours. Both started from a clear UTC instant. The function returns numbers for the checked exercise, but a user interface would usually format the complete local date, time, and zone. Do not assume an offset alone is a permanent identity for a place. An IANA name is what lets the library apply date-specific rules. The function rejects naive input before conversion, because interpreting that input as the host's local time would violate its contract. An unknown zone name raises ZoneInfoNotFoundError; the program should not silently pretend it is UTC.
- 04:55
If this code fails in the browser with that error for a valid zone, check whether the Python runtime includes timezone data and add tzdata there. This is an environment dependency, not a reason to hard-code a fixed offset. In a real scheduling product, preserve the original instant and, when relevant, the user's intended zone name. Future timezone rules can change, so explicit data and tested conversions are safer than an arithmetic shortcut. These checks require different seasonal offsets for one named zone, an unchanged UTC hour in the UTC zone, and rejection of a timestamp with no offset.
- 05:36
The success line appears only if each assertion passes. Notice that the code does not test every daylight saving transition or every historical rule. It tests the contract relevant to these examples and makes the fixed-offset mistake visible. If the valid named zone is unavailable, install or bundle IANA timezone data in the runtime used by the lesson. The browser task should not be marked ready until its actual console can load the zone. After completing the exercise, try changing the UTC hour and predict the local hour before running it. Avoid moving the calendar date across midnight without checking it too; a full application should return the complete local datetime, not just the hour that this focused task returns.
- 06:24
The conceptual model is a single instant with multiple local displays. Aware UTC input identifies the instant, and ZoneInfo chooses the display according to named-zone rules. Manual subtraction changes numbers without applying those rules and can even leave a misleading timezone label. When people depend on scheduled times, this distinction is more than a formatting detail.
Understand the concept
A UTC instant identifies one moment. A wall-clock time such as 9:00 without a time zone does not tell us which instant it means. datetime objects may be naive, with tzinfo=None, or aware, with a timezone. Use aware datetimes at the conversion boundary. zoneinfo.ZoneInfo provides IANA named zones such as America/New_York, whose UTC offset can change with daylight saving rules.
Do not hard-code New York as UTC minus five hours throughout the year. In January it commonly uses EST at UTC-05:00, while in July it uses EDT at UTC-04:00. Converting from an already aware UTC instant with astimezone chooses the appropriate local offset for that date and avoids guessing whether a repeated or missing local wall-clock time was intended.
The task local_hour(utc_text, zone_name) parses an ISO timestamp with an explicit offset. It rejects naive text, converts the instant with ZoneInfo(zone_name), and returns (hour, UTC_offset_seconds). The task checks winter and summer instants and a zone without daylight saving. The caller supplies valid IANA zone names; an unavailable name raises ZoneInfoNotFoundError rather than being silently treated as UTC.
A deployment needs an IANA timezone database. Python can read a system database or the separate tzdata package. Browser Python environments may need that data installed or bundled before named zones work. The examples should be tested in the intended browser runtime as well as local Python. For future appointments, store the intended zone name as well as the instant because timezone rules can change.
- Naive datetimes have no timezone context
- Aware UTC instants can be converted with astimezone
- Named zones encode seasonal offsets
- ZoneInfo needs IANA timezone data
See it step by step
Read the code, predict the output, then compare it with the result.
01. Compare one instant in winter and summer
from datetime import datetime
from zoneinfo import ZoneInfo
zone = ZoneInfo('America/New_York')
for text in ['2026-01-15T12:00:00+00:00', '2026-07-15T12:00:00+00:00']:
local = datetime.fromisoformat(text).astimezone(zone)
print(local.strftime('%H:%M %Z %z'))07:00 EST -0500
08:00 EDT -0400The same UTC hour maps to different New York wall-clock hours as the named zone's offset changes.
02. Distinguish naive from aware timestamps
from datetime import datetime
for text in ['2026-01-15T12:00:00', '2026-01-15T12:00:00+00:00']:
value = datetime.fromisoformat(text)
print(value.tzinfo is None, value.utcoffset())True None
False 0:00:00The first string carries no offset; the second explicitly describes a UTC instant.
A closer look
Follow the reasoning, inspect each result, then try the suggested changes in the console below.
Start from an instant with an explicit UTC offset
A timestamp with +00:00 identifies a UTC instant. A timestamp without an offset is naive and cannot identify an instant by itself. datetime.fromisoformat preserves that distinction. Check tzinfo and utcoffset before converting; a naive value silently interpreted as the host's local time can produce different answers on two computers.
In the controlled example, the aware timestamp has a zero offset. This says nothing yet about New York's wall-clock hour. That second step needs a named zone with historical and seasonal rules. It is better to keep the original UTC instant and derive display times for users than to overwrite the source with an unlabeled local clock reading.
from datetime import datetime
for text in ['2026-01-15T12:00:00', '2026-01-15T12:00:00+00:00']:
value = datetime.fromisoformat(text)
print(value.tzinfo is None, value.utcoffset())True None
False 0:00:00- The first timestamp is naive.
- The second carries an explicit UTC offset.
- Only the aware value is ready for reliable zone conversion.
Let a named zone choose the seasonal offset
America/New_York is not one permanent numeric offset. Winter dates commonly use EST at UTC minus five hours; summer dates use EDT at UTC minus four. Convert the same UTC clock hour on two different dates and inspect both the local hour and the formatted zone label.
Because each input is already an aware UTC instant, astimezone can choose the appropriate local representation without asking whether a wall-clock time was skipped or repeated. Named-zone rules require IANA timezone data. On systems without it, install or bundle tzdata and test that the intended runtime can load the requested zone.
from datetime import datetime
from zoneinfo import ZoneInfo
zone = ZoneInfo('America/New_York')
for text in ['2026-01-15T12:00:00+00:00', '2026-07-15T12:00:00+00:00']:
local = datetime.fromisoformat(text).astimezone(zone)
print(local.strftime('%H:%M %Z %z'))07:00 EST -0500
08:00 EDT -0400- Both inputs are unambiguous UTC instants.
- ZoneInfo supplies the date-specific New York offset.
- The same UTC hour displays as seven or eight locally.
Reject naive text and expose the offset in the result
The practice function parses the input, rejects a missing offset, then converts through ZoneInfo. Returning both local hour and offset seconds makes the seasonal difference easy to test without relying on localized formatting. A January noon UTC instant becomes hour seven at minus eighteen thousand seconds in New York.
The checker also tests July and UTC itself. It expects a named IANA zone to be available; if the browser console lacks zoneinfo data, the runtime must add tzdata before this exercise can be considered usable. Unknown zone names should raise the standard ZoneInfoNotFoundError rather than falling back to a made-up offset.
from datetime import datetime
from zoneinfo import ZoneInfo
def local_hour(utc_text, zone_name):
value = datetime.fromisoformat(utc_text)
if value.tzinfo is None or value.utcoffset() is None:
raise ValueError('timestamp needs an offset')
local = value.astimezone(ZoneInfo(zone_name))
return local.hour, int(local.utcoffset().total_seconds())
print(local_hour('2026-01-15T12:00:00+00:00', 'America/New_York'))
print(local_hour('2026-07-15T12:00:00+00:00', 'America/New_York'))(7, -18000)
(8, -14400)- Parse and reject missing timezone context.
- Convert the aware instant into the named zone.
- Return an observable local hour and numeric offset.
Try it in Python
Edit the example and run it. Python starts in your browser the first time you click Run.
Python console
Ready to runNeed input()? Add one value per line
Your output appears here.
Need a hint?
Use datetime.fromisoformat, check value.tzinfo is not None and value.utcoffset() is not None, then call value.astimezone(ZoneInfo(zone_name)).
Complete the task and select Check task to verify your code.
Quick quiz
Three questions. You can change your answers and try again.
Typical mistakes
Everyone meets these errors. See what causes them and how to fix them.
Hard-coding a winter offset for summer
local = utc_time - timedelta(hours=5)local = utc_time.astimezone(ZoneInfo('America/New_York'))What happens: A July New York time is one hour wrong.
Named-zone conversion chooses the offset for the date.
Treating a naive timestamp as UTC
value = datetime.fromisoformat('2026-01-15T12:00:00')
print(value.astimezone(ZoneInfo('America/New_York')))value = datetime.fromisoformat('2026-01-15T12:00:00+00:00')
print(value.astimezone(ZoneInfo('America/New_York')))What happens: Python interprets the naive time using the host's local timezone.
Provide an explicit offset before converting a moment.