Datetime and Time Zones
Represent dates and instants accurately, and keep time-zone assumptions visible at system boundaries.
By the end, you’ll be able to…
- Choose date, datetime, and timedelta for distinct meanings.
- Use aware UTC datetimes for instants.
- Distinguish time-zone conversion from attaching an interpretation.
01Choose the type from the meaning
A birthday or an invoice due date may be a calendar date without a time zone. Use date for that meaning. An event recorded at a particular moment needs datetime, while an elapsed amount such as ninety minutes uses timedelta. Mixing these meanings encourages accidental midnight assumptions and off-by-one-day displays.
Construct explicit values for examples and tests rather than depending on the current clock. date.fromisoformat parses an agreed ISO date representation; isoformat returns a suitable interchange string. Display formats belong near the interface, while calculations should continue using typed date and time values.
02An instant needs an offset interpretation
A naive datetime has no usable UTC offset information. The value 09:00 alone cannot identify one global instant because many locations have that wall time. An aware datetime carries enough offset information for comparisons and conversions. For recorded events, use datetime.now(UTC) in real code or an explicit tzinfo=UTC in a deterministic test.
Python 3.12 deprecates utcnow(), which returns a naive value despite its name. Prefer an aware UTC value. Comparing naive and aware datetimes for ordering raises TypeError; do not fix that by blindly dropping tzinfo. Decide what the original value meant and normalize data at the boundary.
03Convert an instant rather than relabeling it
astimezone converts the display of an existing instant into another zone. replace(tzinfo=...) attaches or replaces an interpretation without adjusting the wall-clock fields. Attaching UTC to a naive value is correct only when you already know that value represents UTC. Applying it to an unknown local time invents information.
A fixed offset is sufficient for a known +05:30 representation but does not model changing regional rules. zoneinfo.ZoneInfo uses named IANA zones and their historical and daylight-saving rules. It relies on available time-zone data; some platforms need a separately installed data package. The deterministic example uses a fixed offset so it requires only the standard library.
04Measure durations with the right clock
Subtract UTC-normalized aware instants for an elapsed timeline duration, especially around daylight-saving transitions. A timedelta has days and seconds components; use total_seconds() for the complete duration rather than seconds, which excludes whole days. Calendar scheduling such as "every day at nine locally" has different requirements from "every twenty-four elapsed hours".
For benchmarking or timeout elapsed time, use time.perf_counter rather than wall-clock datetimes. Wall time can be adjusted, while a performance counter is intended for measuring intervals. Inject a clock into business functions that ask whether a deadline has passed so tests can supply a fixed now instead of sleeping. Include the exact deadline boundary in those tests.
The ideas worth keeping.
- Model dates, instants, and durations as different concepts.
- Keep event timestamps aware and normalize instants to UTC.
- Use conversion for instants and explicit policies for ambiguous local input.
Make it yours.
Type and test your code right here in the interactive workspace. Run your solution, check the output, and compare with the worked guide when ready.
Normalize a timestamp
Parse 2026-10-20T14:30:00+05:30 and convert it to UTC. Reject a timestamp that lacks an offset instead of assuming a local zone.
Explore a worked solution
from datetime import UTC, datetimedef parse_instant(text): value = datetime.fromisoformat(text) if value.tzinfo is None or value.utcoffset() is None: raise ValueError("timestamp must include an offset") return value.astimezone(UTC)result = parse_instant("2026-10-20T14:30:00+05:30")assert result.isoformat() == "2026-10-20T09:00:00+00:00"try: parse_instant("2026-10-20T14:30:00")except ValueError: print("offset required")The parser establishes a datetime value. The application then enforces awareness and normalizes the same instant into UTC.
Test overdue logic without a live clock
Write is_overdue(deadline, now) for aware values. A task is overdue only when now is strictly later. Test equality and one second later.
Explore a worked solution
from datetime import UTC, datetime, timedeltadef is_overdue(deadline, now): for value in (deadline, now): if value.tzinfo is None or value.utcoffset() is None: raise ValueError("aware datetime required") return now.astimezone(UTC) > deadline.astimezone(UTC)deadline = datetime(2026, 10, 20, 9, tzinfo=UTC)assert not is_overdue(deadline, deadline)assert is_overdue(deadline, deadline + timedelta(seconds=1))Injecting now makes the boundary deterministic. Explicit UTC conversion states that the comparison concerns instants on the timeline.
What stayed with you?
Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.
Normalize a timestamp
Parse 2026-10-20T14:30:00+05:30 and convert it to UTC. Reject a timestamp that lacks an offset instead of assuming a local zone.
offset required
Keep exploring.
The official references behind this chapter, when you want a second angle or the full details.
One chapter closer.
Reading is a start. Trying it yourself makes the connection.