Command Palette

Search for a command to run...

Hectal
PHASE 7Intermediate ~40 min· topic 11 of 12

Topic 7.11

Dates, Times and Time Zones

In one line

Python's datetime module has types for a date, a time of day, both together, and a length of time. Keep every moment as a time-zone-aware value, store and compare in UTC, show local time only at the edge, and write dates as ISO 8601 text.

Think of it like this

A phone call between two cities. You say "let's talk at 3 o'clock", and your friend, in another country, says "3 o'clock when?" The number 3 means nothing until you say whose clock. A date and time written without a time zone is the same: it names a spot on a clock face, not a moment in history. Programs that forget the "whose clock" part book meetings at the wrong hour.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

datetime
A Python object holding a date and a time of day, and optionally a time zone.
timedelta
A length of time, such as 2 days and 3 hours. It is not a point on the calendar.
Naive datetime
A datetime with no time zone. It is just a clock reading, so it is ambiguous.
Aware datetime
A datetime that knows its offset from UTC, so it names one exact moment.
UTC
Coordinated Universal Time, the world's reference clock. It has no daylight saving.
Offset
How far a local clock is from UTC, such as +05:30 for India.
Daylight saving time (DST)
Moving the clocks forward in spring and back in autumn in some places.
ISO 8601
The international way to write dates and times: 2026-03-08T14:30:05.
Epoch and timestamp
The epoch is 00:00 UTC on 1 January 1970. A timestamp counts the seconds since then.
Monotonic clock
A clock that only moves forward and is not changed by anyone, so it is safe for measuring durations.

Step by step

01Four building blocks

date is a day, time is a time of day, datetime is both, and timedelta is a distance between two moments. The numbers go in from the largest unit to the smallest: year, month, day, hour, minute, second.

In the code below, moment + timedelta(...) moves the moment forward, and one date minus another gives the gap between them. Also useful: date(2026, 3, 8).weekday() gives 0 for Monday up to 6 for Sunday, and strftime("%A") gives the name.

main.pywhole filepython
from datetime import date, datetime, timedelta

moment = datetime(2026, 3, 8, 14, 30, 5)
print(moment + timedelta(days=2, hours=3))
print(date(2026, 12, 25) - date(2026, 10, 10))

02Subtracting gives a timedelta

The difference between two dates is a timedelta. It has .days, .seconds and .total_seconds(). Careful: .seconds is only the leftover seconds after whole days. So timedelta(days=2, hours=3).seconds is 10800 (three hours), not 183600. Use total_seconds() for the whole length.

There is no timedelta(months=1), because months have different lengths. Add 30 days, or compute the first day of the next month yourself. Libraries such as python-dateutil (relativedelta) and pandas offer calendar-month arithmetic.

03Text in, text out: strftime and strptime

strftime formats; strptime parses. They use the same codes, so you can check one against the other.

Use strptime only for the formats other people give you (25/12/2026). For your own storage use ISO 8601 and isoformat / fromisoformat.

main.pywhole filepython
from datetime import datetime

moment = datetime(2026, 3, 8, 14, 30, 5)
print(moment.strftime("%d %B %Y, %I:%M %p"))

parsed = datetime.strptime("25/12/2026 09:15", "%d/%m/%Y %H:%M")
print(parsed.isoformat())
terminal
$ python main.py
── expected output ──
08 March 2026, 02:30 PM
2026-12-25T09:15:00

04Naive and aware

Give a datetime a zone and it becomes a real moment. timezone.utc is the built-in UTC. ZoneInfo("Asia/Tokyo") is a named zone with its full history of rules.

Two aware values are equal when they are the same instant, even if they show different clock times: noon UTC equals 21:00 in Tokyo.

main.pywhole filepython
from datetime import datetime, timezone
from zoneinfo import ZoneInfo

utc = datetime(2026, 7, 1, 12, 0, tzinfo=timezone.utc)
tokyo = utc.astimezone(ZoneInfo("Asia/Tokyo"))
print(tokyo)
print(utc == tokyo)
terminal
$ python main.py
── expected output ──
2026-07-01 21:00:00+09:00
True
Naive and awarediagram
Rendering diagram…

05Why the name of the zone matters

In London in January the offset is +00:00 (GMT). In July it is +01:00 (BST). A zone is not one number; it is a rule book with history. ZoneInfo carries the rule book, so the same wall-clock time gets the right offset for its date.

On Linux and macOS, zoneinfo reads the system's time-zone files. On Windows there are none, so install the tzdata package (pip install tzdata). In the browser on this page the time-zone data loads for you.

06The daylight-saving trap

On 8 March 2026 New York's clocks jump from 02:00 to 03:00, so that day has only 23 hours. Add timedelta(days=1) to noon on 7 March and Python keeps the wall clock: you get noon on 8 March, which is really only 23 hours later. Two aware values with the same tzinfo are subtracted by wall clock, so Python even reports 24 hours.

To add real elapsed time, convert to UTC, add, and convert back. There are two other oddities. A time can not exist (02:30 on that spring morning). A time can happen twice (01:30 on 1 November 2026, when clocks go back). Python accepts both. The fold attribute (0 or 1) chooses the first or second occurrence. Best practice: avoid local arithmetic across a DST change and keep everything in UTC.

07Timestamps, the epoch, and measuring time

A timestamp is one number, so it is easy to store and compare. datetime.fromtimestamp(0, tz=timezone.utc) is the epoch: 1970-01-01 00:00 UTC. Always pass tz=; without it you get a naive local time.

To time a piece of code use time.perf_counter() (best resolution) or time.monotonic(). Both are clocks that only go forward. The wall clock (datetime.now()) can be adjusted by the operating system or network time sync, even backwards, which would give negative durations.

main.pywhole filepython
import time

start = time.perf_counter()
total = sum(range(100_000))
elapsed = time.perf_counter() - start
print(f"took {elapsed:.4f} seconds")   # the number differs on every run

Try it yourself

  1. 1

    Predict the weekday

    In the first example, change the date to your birthday this year and write down the weekday before you run it. Then change the format to %a %d %b %Y and see the short names.

  2. 2

    Move a meeting around the world

    In the second example, create datetime(2026, 7, 1, 15, 0, tzinfo=ZoneInfo("Asia/Kolkata")) and convert it to UTC, America/New_York and Australia/Sydney. Work out the answers on paper first. Sydney is on winter time in July, so the offset is not what you might expect.

  3. 3

    Cross the clock change yourself

    In the daylight-saving example, change the start date to datetime(2026, 3, 28, 12, 0, tzinfo=london). London's clocks go forward on 29 March 2026. Predict whether + timedelta(days=1) and a UTC-based 24 hours still agree, then check.

Code & diagrams

Dates, timedeltas, formatting and parsing New tab

Every date here is fixed, so the output is the same on every run. Change the format codes and predict the text first.

Sign in to run this example in your browser.

Expected output

date: 2026-03-08
time: 14:30:05
datetime: 2026-03-08 14:30:05
parts: 2026 3 8 14 30
weekday (Mon=0): 6 Sunday
later: 2026-03-10 17:30:05
difference: 76 days, 0:00:00
days only: 76
total seconds: 183600.0
08 March 2026, 02:30 PM
2026-03-08
parsed: 2026-12-25 09:15:00 datetime
2026-03-08T14:30:05
2026-03-08 14:30:05
True
ValueError: time data '2026-13-01' does not match format '%Y-%m-%d'
Aware datetimes, zoneinfo and the daylight-saving trap New tab

Look at the two lines for 8 March: the wall clock says noon, the real 24 hours say one o'clock. Then read the two 1:30 lines of 1 November.

Sign in to run this example in your browser.

Expected output

naive tzinfo: None
aware: 2026-07-01 12:00:00+00:00 UTC
in London: 2026-07-01 13:00:00+01:00
in Tokyo : 2026-07-01 21:00:00+09:00
same instant: True
TypeError: can't compare offset-naive and offset-aware datetimes
2026-01-15T09:00:00+00:00 GMT 0:00:00
2026-07-15T09:00:00+01:00 BST 1:00:00
wall-clock +1 day: 2026-03-08 12:00:00-04:00
real 24 hours    : 2026-03-08 13:00:00-04:00
hours between wall times: 24.0
hours really passed     : 23.0
2:30 in the gap: 2026-03-08T02:30:00-05:00 -> UTC 2026-03-08T07:30:00+00:00
1:30 first : 2026-11-01T01:30:00-04:00 EDT
1:30 second: 2026-11-01T01:30:00-05:00 EST
Timestamps, ISO text with offsets, monotonic clock New tab

The timing line prints only true or false, because the real duration changes on every run.

Sign in to run this example in your browser.

Expected output

1970-01-01 00:00:00+00:00
timestamp: 1791633600.0 1791633600
back: 2026-10-10T12:00:00+00:00
2026-10-10 12:00:00+00:00
2026-10-10 17:30:00+05:30
2026-10-10 12:00:00+00:00
sum: 4999950000
elapsed is a non-negative float: True
monotonic never goes back: True
stored: 2026-10-10T12:00:00+00:00
shown : 10 Oct 2026 17:30 IST
Calendar facts that surprise people New tab

A month is not 30 days, 12/10/2026 has two meanings, and replace() labels while astimezone() converts.

Sign in to run this example in your browser.

Expected output

2026-03-02
28 31
-7
2026-10-12 2026-12-10
2028-02-29 2027-03-01
ValueError: day is out of range for month
ZoneInfoNotFoundError 'No time zone found with key Mars/Olympus'
2026-07-01T09:00:00+05:30
2026-07-01T04:30:00+01:00
Store UTC, show localdiagram
Rendering diagram…
Today's date: correct, but not repeatablepython

Not runnable with a fixed output: it prints the moment you run it. Prefer an aware now(). datetime.utcnow() and utcfromtimestamp() are deprecated since Python 3.12 because they return a naive value in UTC, which is easy to mistake for local time.

from datetime import datetime, timezone

now_utc = datetime.now(timezone.utc)      # aware, in UTC: use this
today = datetime.now().date()             # naive, local calendar day
print(now_utc.isoformat())

# Avoid:
# datetime.utcnow()   # naive value that is really UTC (deprecated since Python 3.12)
Real use: how long did each step of a request take? New tab

Log lines usually start with an ISO 8601 time in UTC. `fromisoformat` reads it, and subtracting two aware values gives a timedelta. `zip(times, times[1:])` pairs each line with the next one (Topic 2.6).

Sign in to run this example in your browser.

Expected output

'model replied' came 2 s after the previous line
'request finished' came 83 s after the previous line
whole request took 0:01:25

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Compare a naive time with an aware one

Compare a deadline that has a zone with a submission time that does not.

main.pywhole filepython
from datetime import datetime, timezone

deadline = datetime(2026, 7, 1, 12, 0, tzinfo=timezone.utc)
submitted = datetime(2026, 7, 1, 11, 0)
print(submitted < deadline)
terminal
$ python main.py
── what you'll see ──
Traceback (most recent call last):
File "main.py", line 5, in <module>
print(submitted < deadline)
^^^^^^^^^^^^^^^^^^^^
TypeError: can't compare offset-naive and offset-aware datetimes

Break #2

Parse a date with the wrong format

Parse a day/month/year string with a year-first format.

main.pywhole filepython
from datetime import datetime

print(datetime.strptime("10/12/2026", "%Y-%m-%d"))
terminal
$ python main.py
── what you'll see ──
ValueError: time data '10/12/2026' does not match format '%Y-%m-%d'

Myth vs fact

Myth

replace(tzinfo=...) converts a time to that zone.

Fact

It only attaches a label and leaves the clock numbers alone. astimezone(zone) is the conversion. Labelling a UTC value as Tokyo gives a time nine hours wrong.

Myth

A day always has 24 hours, and timedelta(days=1) is always the same as 24 hours.

Fact

In zones with daylight saving, one day a year has 23 hours and another has 25. Python's local arithmetic keeps the wall clock; only UTC arithmetic gives true elapsed time.

Myth

Storing local time is fine if all my users are in one country.

Fact

Even one country changes its clocks, and users travel. Server logs, deployments and cloud services mostly run in UTC. Store UTC and convert when displaying.

Myth

datetime.now() is the safe way to get the current time.

Fact

It returns a naive local time, which is easy to misread later. Use datetime.now(timezone.utc) for an aware value.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Python's datetime stores whole microseconds. Parsing text with nanoseconds, as many logging systems and databases produce, loses digits: fromisoformat in 3.11 and later silently truncates after six fractional digits, and older versions reject the text, so decide how to round on purpose. Databases often store timestamp with time zone as UTC and return it in the session's zone; check what your driver gives you.

  • ▸

    zoneinfo reads the IANA time-zone database, which is updated several times a year as governments change rules. Servers and containers need current tzdata (the system package or the tzdata package on PyPI). Keep long-lived timestamps in UTC and the rules never matter for storage; they matter only when you convert a *future* local time, because the rules for next year may still change.

  • ▸

    For future events (a meeting at 09:00 in Berlin next October) store the local time and the zone name, not the UTC instant, because if the rules change, the instant should move with them. For past events (a payment happened) store the UTC instant.

  • ▸

    Version differences that bite. Before Python 3.11, fromisoformat could only read what isoformat writes, so "2026-10-10T12:00:00Z" raised ValueError (people wrote .replace("Z", "+00:00")); 3.11 accepts Z and most ISO 8601 shapes. datetime.utcnow() and utcfromtimestamp() are deprecated since 3.12 because they return a naive value that merely holds UTC numbers. Use datetime.now(timezone.utc), or datetime.UTC (an alias of timezone.utc since 3.11).

  • ▸

    More traps. naive.timestamp() assumes the naive value is in the machine's local zone, so the same code gives different numbers on a laptop and a UTC server. strptime with %y maps 69 to 1969 and 68 to 2068, so prefer four-digit years. %Z parses only a few names, so do not use it for real zone handling. With zoneinfo, replace(tzinfo=ZoneInfo(...)) is the correct way to label a naive local time (unlike the old pytz, which needed localize). datetime has no leap seconds: 23:59:60 raises ValueError.

  • ▸

    Pandas uses its own Timestamp and datetime64[ns] types with the same ideas (naive versus tz-aware, tz_localize to label and tz_convert to convert). time.perf_counter() is the best clock for short measurements; time.time() is the wall clock in seconds since the epoch and can jump.

Remember this

  1. 1

    The datetime module (you bring it in with from datetime import ..., Topic 6.1) gives you four building blocks. date(2026, 3, 8) is a calendar day. time(14, 30) is a time of day. datetime(2026, 3, 8, 14, 30) is both. timedelta(days=2, hours=3) is a length of time, not a point in time. Adding a timedelta to a datetime moves it. Subtracting two datetime values gives a timedelta. All of these are immutable (they never change; Topic 3.5): every calculation returns a new object.

  2. 2

    To turn a date into text, use strftime ("string from time") with codes. %Y is the four-digit year, %m the month number and %d the day. %H is the hour on a 24-hour clock, and %I with %p gives a 12-hour clock with AM or PM. %A is the weekday name and %B the month name. To turn text into a date, use strptime ("string parsed into time") with the same codes. Python raises ValueError if the text does not fit the format. The month and day order is not guessed: 12/10/2026 is 12 October in %d/%m/%Y and 10 December in %m/%d/%Y.

  3. 3

    For storing and sending dates, use ISO 8601: 2026-03-08T14:30:05, year first, largest unit to smallest. It sorts correctly as plain text, nobody can confuse day and month, and every language and database understands it. dt.isoformat() writes it and datetime.fromisoformat(text) reads it. Since Python 3.11, fromisoformat also reads a trailing Z (meaning UTC) and most other ISO 8601 shapes.

  4. 4

    A datetime is either naive (no time zone: tzinfo is None) or aware (it knows its offset from UTC, meaning how many hours and minutes its clock is ahead of or behind UTC). A naive value is only a clock reading and is ambiguous. Python refuses to order or subtract a naive and an aware value (<, > and - raise TypeError: can't compare offset-naive and offset-aware datetimes). == does not raise but always answers False. That is a kindness, because it stops a silent bug. Make values aware as early as possible, ideally the moment they enter your program.

  5. 5

    zoneinfo (Python 3.9) knows the world's time zones by name: ZoneInfo("Europe/London"). Use names, never a hand-typed offset like +01:00. The offset of one place changes during the year (daylight saving time, abbreviated DST) and over the years (governments change the rules). dt.astimezone(zone) converts an aware value to another zone without changing the instant (the one exact moment in history). dt.replace(tzinfo=zone) only attaches a label to a naive clock reading and changes nothing else. Mixing the two up is a very common bug.

  6. 6

    The golden rule: inside your program use UTC. Convert to a local zone only when showing a time to a person, and convert back to UTC when you receive one. Store UTC in files and databases.

  7. 7

    A timestamp (also Unix time or epoch time) is a count of seconds since 00:00 UTC on 1 January 1970, the epoch. dt.timestamp() and datetime.fromtimestamp(seconds, tz=timezone.utc) convert. To measure how long something took, never subtract two wall-clock times (the wall clock is the ordinary clock on the wall, which can be corrected or jump for DST). Use time.monotonic() or time.perf_counter(), which only move forward. Timestamps and logs in AI systems follow the same rule: UTC, ISO 8601. Examples are the time a model call started and when a document was crawled.

Explain it without notes

01

What is the difference between a naive and an aware datetime, and why does Python refuse to compare them?

02

Why should a program store times in UTC, and when should it convert to a local zone?

03

What is the difference between astimezone and replace(tzinfo=...)?

04

Describe a daylight-saving bug and how to avoid it.

05

Why use time.monotonic() or perf_counter() to measure a duration instead of two datetime.now() calls?

Practice

01

Write code that prints how many days are left between 10 October 2026 and 25 December 2026, in the form Days left: 76. Hint: subtract two date objects and read .days.

02

Parse the text "03 Mar 2026 18:45" with strptime and print the result in ISO 8601 (it should print 2026-03-03T18:45:00). Hint: %b is the short month name.

03

A meeting is at 15:00 in Asia/Kolkata on 1 July 2026. Print its time in UTC (HH:MM) and in America/New_York (HH:MM followed by the zone abbreviation, from %Z). Hint: make the meeting aware first, then use astimezone.

04

Write last_day(year, month) that returns the last date of a month. Find the first day of the next month and subtract one day. Print last_day(2028, 2) and last_day(2026, 12) on one line. Hint: December needs the year to go up by one and the month to wrap to 1.

Trade-offs

  • ↔

    Aware UTC everywhere versus local time. UTC is simple, comparable and safe, but must be converted for display. Local time reads naturally but is full of traps. Use UTC inside and local at the edges. The exception is a future event in a specific place, where you keep the local time and the zone name.

  • ↔

    Standard library versus helper libraries. datetime and zoneinfo are enough for most work and have no dependencies. python-dateutil, pendulum and arrow add friendlier parsing and calendar-month arithmetic, at the price of one more dependency and another API to learn.

  • ↔

    ISO 8601 text versus timestamps. ISO text is readable and carries the offset; a number is compact and easy to compare but unreadable in a log. Pick one per system and stick to it.

Done when you can

  • Done when you can build and subtract date, datetime and timedelta values and read .days and total_seconds().

  • Done when you can format with strftime, parse with strptime and read and write ISO 8601 with fromisoformat and isoformat.

  • Done when you can explain naive versus aware and convert between zones with ZoneInfo and astimezone.

  • Done when you can explain a daylight-saving bug with a real example and avoid it with UTC.

  • Done when you can convert a timestamp to a datetime and back, and time code with perf_counter.