Data that crosses systems is where time zone mistakes become permanent.
A small set of conventions avoids most problems:
- Instants (when something happened: created_at, log lines, payments): store in UTC. In databases, use a time-zone-aware column type such as PostgreSQL
timestamptz, and pass aware datetimes to the driver. In JSON and logs, write ISO 8601 with an offset:2025-03-01T12:00:00+00:00or with aZ. - Future local events (a meeting at 9:00 in Berlin next October): store the local date-time and the IANA zone name. Time zone rules may change before then; converting to UTC now could freeze the wrong offset.
- Calendar dates (birthdays, due dates): store a
date, not a datetime at midnight, which shifts a day when converted between zones.
Convert to the user's local zone only at the edges, when displaying.
When reading data, decide explicitly what naive values mean. A CSV with 2025-03-01 09:00 is ambiguous; attach the source's zone with replace(tzinfo=ZoneInfo(...)) at the boundary and keep everything aware inside.
JSON has no date type. Serialise with isoformat() and parse with fromisoformat(); for numeric APIs, document whether timestamps are seconds or milliseconds.
Example
import json
from datetime import UTC, date, datetime
from zoneinfo import ZoneInfo
event = {
"created_at": datetime(2025, 3, 1, 12, 0, tzinfo=UTC).isoformat(),
"starts_local": "2025-10-28T09:00",
"zone": "Europe/Berlin",
"due": date(2025, 4, 1).isoformat(),
}
text = json.dumps(event)
data = json.loads(text)
created = datetime.fromisoformat(data["created_at"])
start = datetime.fromisoformat(data["starts_local"]).replace(tzinfo=ZoneInfo(data["zone"]))
print(created.tzinfo, start.utcoffset()) # UTC 1:00:00
print(start.astimezone(UTC).isoformat()) # 2025-10-28T08:00:00+00:00
print(date.fromisoformat(data["due"])) # 2025-04-01Remember
- Store instants in UTC with an offset in ISO 8601
- Store future local events as local time plus IANA zone name
- Use
datefor calendar dates and convert to local time only for display
Watch out for
- Storing a birthday as a UTC datetime at midnight, which displays as the previous day in western time zones.