When Python code works with times across time zones, the correct approach is to always use “aware” datetimes — datetimes with tzinfo attached — with zoneinfo.ZoneInfo, store times in UTC, and convert to local time only for display. This applies whenever you save timestamps to a database, write logs, schedule appointments, or compare times across time zones.
The problem
A bare datetime.now() returns a “naive” datetime — attached to no time zone at all. A naive time looks just like a real time, but Python does not know which time zone it belongs to, so you cannot reliably convert it to another time zone. Worse, comparing a naive datetime with an aware one (for example a timestamp from an API carrying +07:00) raises an error immediately:
from datetime import datetimedeadline = datetime(2026, 11, 4, 16, 0) # naive, no time zonenow = datetime.now() # also naive# Timestamp from an API, with a time zone attachedfrom_api = datetime.fromisoformat("2026-11-04T16:00:00+07:00")print(now > from_api) # TypeError: can't compare offset-naive and offset-aware datetimes
Another common mistake is adding timedelta(hours=7) by hand to “convert” time zones. That ignores daylight saving time (DST) and breaks when time zone rules change.
The solution
The zoneinfo module has been in the standard library since Python 3.9 — no extra package needed. The rule is just two lines: always create moments in UTC, and only use astimezone() when you need to display local time:
from datetime import datetime, timezonefrom zoneinfo import ZoneInfovn = ZoneInfo("Asia/Ho_Chi_Minh")# 1. Get "now" as an aware datetime, in UTCnow_utc = datetime.now(timezone.utc)# 2. Convert to Vietnam time only for displaynow_vn = now_utc.astimezone(vn)print(now_utc.isoformat()) # 2026-11-04T08:57:31+00:00print(now_vn.isoformat()) # 2026-11-04T15:57:31+07:00# 3. ISO string with a time zone: parse, then convert with astimezone()dt = datetime.fromisoformat("2026-11-04T16:00:00+07:00")print(dt.astimezone(timezone.utc).isoformat()) # 2026-11-04T09:00:00+00:00# 4. A meeting time typed in by a user (wall time): attach tzinfo at creationmeeting = datetime(2026, 11, 5, 9, 0, tzinfo=vn)print(meeting.astimezone(timezone.utc).isoformat()) # 2026-11-05T02:00:00+00:00
Distinguish two easily confused operations: replace(tzinfo=…) only labels a known wall time with a time zone, while astimezone() is the real conversion between time zones. To go from Vietnam time to UTC, use astimezone(timezone.utc) — never subtract 7 hours by hand.
One environment note: zoneinfo reads the operating system’s time zone database. If you hit ZoneInfoNotFoundError on Windows or a minimal container, just pip-install the tzdata package — no code change needed.
Applying it to your project
In a real project, agree on a single convention: the database and API always store and exchange time in UTC, as aware datetimes or ISO 8601 strings with a time zone. Only at the display layer — templates, notifications, emails — convert to the user’s time zone with astimezone(). That way logs, appointments, and scheduled jobs never drift just because the server sits in a different time zone from the user. In tests, run the same code under two different time zones (for example Asia/Ho_Chi_Minh and UTC) to make sure no manual hour arithmetic remains.
Related posts
- Python: replace os.path with pathlib — read, write, and browse files more concisely in 5 minutes
- Python: write a context manager with @contextmanager — resources always closed, no long class
- Python: replace long if/elif chains with match — pattern matching in 5 minutes
FAQ
What is the difference between naive and aware datetimes?
Naive means a datetime with no tzinfo — Python does not know which time zone it belongs to. Aware means a datetime with tzinfo, which can be converted and compared accurately across time zones.
How should I store times in the database?
Store in UTC, as an aware datetime or an ISO 8601 string with a time zone. When displaying, convert to the user’s time zone with astimezone().
Does daylight saving time make zoneinfo calculate wrong?
No, if you convert with astimezone(). zoneinfo applies the correct offset for each moment. Only manual hour arithmetic breaks under DST.
How do I fix ZoneInfoNotFoundError?
It means the machine lacks a time zone database. Install the tzdata package with pip and zoneinfo will use it with no code change.
💬 Have you tried this in your own project? If you hit an error or know a cleaner way, leave a comment below — I read them all and will fold good ideas into the post.
