Handling Time Zones and DST in Python: zoneinfo, pytz, and datetime

Python's date handling has a genuinely confusing history — three different ways to represent a time zone have been in common use depending on which Python version and which library a given piece of code was written against. Here's what actually works today, and why.

1. Naive vs. Aware: The Distinction That Matters Most

Every datetime object in Python is either "naive" (no time zone attached — Python has no idea what zone it represents) or "aware" (it carries an explicit tzinfo). This is the single most important distinction in Python's date handling: comparing or subtracting a naive datetime and an aware one raises a TypeError, and code that silently mixes the two is one of the most common sources of Python timezone bugs.

from datetime import datetime naive = datetime(2026, 7, 22, 14, 30) # no time zone info at all aware = datetime(2026, 7, 22, 14, 30, tzinfo=None) # still naive — tzinfo=None isn't a zone

2. zoneinfo: The Modern Standard Library Way (Python 3.9+)

As of Python 3.9, the standard library ships zoneinfo, which reads time zone rules straight from the IANA Time Zone Database already installed on the system — the same database this whole site is built on — no third-party package required.

from datetime import datetime from zoneinfo import ZoneInfo now_tokyo = datetime.now(ZoneInfo("Asia/Tokyo")) print(now_tokyo) # 2026-07-22 23:30:00+09:00

3. Why pytz Is on Its Way Out

Before zoneinfo existed, pytz was the de facto standard, but it has a well-known sharp edge: you can't just pass a pytz timezone directly to datetime()'s tzinfo argument the way you can with zoneinfo — doing so silently produces the wrong offset for zones with more than one historical UTC offset. The correct (but easy to forget) pattern requires calling .localize() instead:

import pytz from datetime import datetime # WRONG — silently uses the zone's very first historical offset, not the current one wrong = datetime(2026, 7, 22, 14, 30, tzinfo=pytz.timezone("America/New_York")) # CORRECT — pytz's own localize() computes the right offset for this date tz = pytz.timezone("America/New_York") right = tz.localize(datetime(2026, 7, 22, 14, 30))

zoneinfo doesn't have this footgun — datetime(..., tzinfo=ZoneInfo(...)) just works correctly — which is the main reason the Python core team added it and now recommends it over pytz for anything running Python 3.9+.

4. Converting Between Zones

Once a datetime is aware, converting it to another zone is a single call to .astimezone():

from zoneinfo import ZoneInfo ny_time = datetime.now(ZoneInfo("America/New_York")) tokyo_time = ny_time.astimezone(ZoneInfo("Asia/Tokyo"))

5. Detecting Whether a Zone Observes DST

The same January-vs-July offset comparison used throughout DateTimeX (see the Handling Timezones & UTC in JavaScript guide for the JavaScript version) works identically in Python:

from datetime import datetime from zoneinfo import ZoneInfo tz = ZoneInfo("Europe/London") jan_offset = datetime(2026, 1, 15, tzinfo=tz).utcoffset() jul_offset = datetime(2026, 7, 15, tzinfo=tz).utcoffset() observes_dst = jan_offset != jul_offset # True for London, False for Tokyo

6. Common Pitfalls

  • datetime.utcnow() returns a naive datetime, despite the name — it gives you the current UTC time as a value with no time zone attached, which invites exactly the naive/aware mixing bug from section 1. Prefer datetime.now(ZoneInfo("UTC")), which is aware.
  • datetime.now() with no argument uses the system's local time zone as a naive result — fine for quick scripts, risky in server code where "local" means the server's zone, not any user's.
  • Windows systems may not ship the IANA time zone database by default, so zoneinfo on Windows can raise ZoneInfoNotFoundError unless the tzdata package (pip install tzdata) is installed — Linux and macOS almost always have it preinstalled at the OS level.

Related

The same core techniques — offset lookup, DST detection, zone conversion — are covered for JavaScript in Handling Timezones & UTC in JavaScript. To sanity-check a specific conversion by eye, use our DST Tracker.

Tools

Articles & Guides