Managing Dates and Times

Python's datetime module is the standard way to represent points in time and durations. The main classes are date (year/month/day), time (hour/minute/second), datetime (both combined), and timedelta (a span between two instants). Pick the smallest class that covers your need — a birthday is a date, a reminder is a datetime, a countdown is a timedelta.

The single most important distinction is aware vs naive. An aware datetime carries a tzinfo (a timezone); a naive one does not. Mixing them in arithmetic raises TypeError. Real-world software should be aware by default: use datetime.now(timezone.utc) for internal timestamps and convert to local time only for display. Storing naive datetimes “in UTC” is a subtle bug waiting to happen.

Parsing and formatting are where most date code happens. datetime.fromisoformat("2025-01-31T12:30:00+00:00") reads ISO 8601, the default machine-friendly format. dt.strftime("%Y-%m-%d") formats with codes (%Y year, %m month, %d day, %H hour, %M minute, %S second); strptime is its inverse. Prefer ISO format for anything not displayed to users.

Arithmetic on datetimes is expressed in timedelta. dt + timedelta(days=7) moves a week forward; dt2 - dt1 returns a timedelta. For month or year math you need dateutil.relativedelta, because calendar months don't have a fixed duration. Never add “30 days” when you mean “one month”.

Aware datetimes are the default

Build with datetime.now(timezone.utc) on the way in, convert to local with dt.astimezone(zoneinfo.ZoneInfo("Europe/Oslo")) on the way out. Serialize with dt.isoformat(); parse with datetime.fromisoformat(...).

Python 3.9+ ships zoneinfo, the official time zone database. Before 3.9 you had to install pytz; new code should always use zoneinfo.

Durations and formatting

timedelta(days=D, hours=H, seconds=S) is an ordinary object you can add to datetimes and multiply by integers. Use it to encode intervals explicitly rather than sprinkling magic numbers through the code.

For format strings, cheat sheet: %Y-%m-%d %H:%M:%S is the most common non-ISO timestamp. %a/%A are weekday short/long names; %b/%B are month names; %z is the UTC offset; %Z is the tz name. Always test the formatted strings you plan to parse back in.

The datetime vocabulary.

ToolPurpose
datetime.date
class
Calendar date with year/month/day.
datetime.datetime
class
Date + time (optionally timezone-aware).
datetime.timedelta
class
Span between two datetimes.
zoneinfo.ZoneInfo
class
Named time zone (Python 3.9+).
datetime.now(tz)
method
Current wall-clock time.
datetime.fromisoformat
method
Parse ISO 8601 strings.
strftime/strptime
methods
Format/parse with explicit codes.
time.monotonic()
function
Monotonic clock for measuring intervals.

Managing Dates and Times code example

The script below creates aware datetimes, does simple arithmetic, formats for display, and measures a short interval.

# Lesson: Managing Dates and Times
import time
from datetime import date, datetime, timedelta, timezone
from zoneinfo import ZoneInfo


utc_now = datetime.now(timezone.utc)
oslo = utc_now.astimezone(ZoneInfo("Europe/Oslo"))
tokyo = utc_now.astimezone(ZoneInfo("Asia/Tokyo"))

print("utc now:", utc_now.isoformat())
print("oslo:   ", oslo.strftime("%Y-%m-%d %H:%M %Z"))
print("tokyo:  ", tokyo.strftime("%Y-%m-%d %H:%M %Z"))

# Arithmetic with timedelta
one_week = timedelta(days=7)
later = utc_now + one_week
print("in 7 days:", later.isoformat())
print("delta:    ", later - utc_now, "|", (later - utc_now).days, "days")

# Parse ISO 8601 round-trip
payload = utc_now.isoformat()
parsed = datetime.fromisoformat(payload)
print("round-trip equal?", parsed == utc_now)

# date-only math
today = date.today()
next_friday = today + timedelta(days=(4 - today.weekday()) % 7)
print("next friday:", next_friday.isoformat())

# Measure an interval with a monotonic clock
start = time.monotonic()
time.sleep(0.05)
elapsed = time.monotonic() - start
print(f"slept for ~{elapsed*1000:.0f} ms")

# Naive + aware mix raises an error, on purpose
naive = datetime(2025, 1, 1)
try:
    naive - utc_now
except TypeError as err:
    print("mix error:", err)

As you read, focus on:

1) `datetime.now(timezone.utc)` is the aware-by-default habit.
2) `astimezone(ZoneInfo(...))` converts without changing the instant.
3) `fromisoformat` / `isoformat` is the canonical round-trip pair.
4) Mixing naive and aware datetimes raises TypeError — that's a feature.

Practice duration and formatted output.

from datetime import datetime, timedelta, timezone

start = datetime(2025, 1, 1, tzinfo=timezone.utc)
end = start + timedelta(hours=36, minutes=15)
delta = end - start

total_minutes = int(delta.total_seconds() // 60)
print(f"{total_minutes} minutes")

fmt = end.strftime("%A %B %d %Y, %H:%M %Z")
print(fmt)

Invariants to lean on.

from datetime import datetime, timedelta, timezone
t = datetime(2025, 1, 1, tzinfo=timezone.utc)
assert (t + timedelta(days=1)).day == 2
assert t.isoformat() == "2025-01-01T00:00:00+00:00"
assert datetime.fromisoformat(t.isoformat()) == t

Running prints something like:

utc now: 2026-04-21T10:00:00+00:00
oslo:    2026-04-21 12:00 CEST
tokyo:   2026-04-21 19:00 JST
in 7 days: 2026-04-28T10:00:00+00:00
delta:     7 days, 0:00:00 | 7 days
round-trip equal? True
next friday: 2026-04-24
slept for ~51 ms
mix error: can't subtract offset-naive and offset-aware datetimes