Intermediate18 min

Dates, times, and timezones

datetime, timedelta, formatting, parsing, and why every timestamp you store should be UTC.

Why this matters in AI / ML / GenAI

Time features drive forecasting and drift detection, log timestamps drive incident analysis, and token-usage reports are grouped by day. Naive local timestamps are a reliable source of off-by-hours bugs across regions.

1. Read

Understand the idea in plain English first.

2. Run

Load any example into the compiler and press Run.

3. Change

Edit one value, rerun, and learn from the output.

The objects

The datetime module gives you date, time, datetime, timedelta, and timezone.

datetime.now() returns local time; datetime.now(timezone.utc) returns UTC. Prefer UTC everywhere in stored data and logs, and convert to local only when displaying to a person.

A datetime is naive (no timezone) or aware (with one). Subtracting a naive from an aware datetime raises TypeError, which is Python protecting you from a meaningless result.

timedelta represents a duration. Subtracting two datetimes gives one; .total_seconds() converts it to a number you can log or compare.

Formatting and parsing

strftime(fmt) formats a datetime as text. strptime(text, fmt) parses text into a datetime.

Common codes: %Y 4-digit year, %m month, %d day, %H hour (24h), %M minute, %S second, %f microseconds, %z UTC offset, %A weekday name, %B month name.

For machine-readable output use ISO 8601: dt.isoformat() produces 2026-09-04T14:30:00+00:00, and datetime.fromisoformat() reads it back. ISO strings sort correctly as plain text, which is why they belong in filenames, JSON, and database columns.

Unix timestamps: dt.timestamp() and datetime.fromtimestamp(ts, tz=timezone.utc).

Practical rules

  1. Store UTC, display local. A user in Hyderabad and one in London must see the same event at their own wall-clock time.
  2. Use aware datetimes at boundaries — anything entering a database or an API.
  3. Never do date arithmetic with timedelta(days=30) and call it "a month". Months vary; use dateutil.relativedelta if you need calendar months.
  4. Measure durations with time.perf_counter(), not datetime.now() differences. perf_counter is monotonic and unaffected by clock adjustments.
  5. Include a timezone in log timestamps, or a cross-region incident becomes unreconstructable.

Hands-on practice

Compiler on the left. Examples on the right.

On desktop, keep the compiler beside the examples. On mobile, the same blocks stack cleanly. Pick an example, try it in the compiler, then change one small thing.

Build a run manifest with timestamps

Live Python compiler

Try it — in-browser Python

Change the training duration and confirm the finish time and rate update.

Code editor

Output

Python runs in your browser. First run downloads the runtime.

Press Run (or Ctrl+Enter) to execute.

Runs CPython in your browser. NumPy, pandas, scikit-learn and Matplotlib load on demand. Charts appear below the output. No input(), no GPU, no network installs.

Clear code examples

Every example is copy-ready. Use Try in compiler when you want to experiment without scrolling around.

Example

Creating and inspecting datetimes

Note the difference between naive local time and aware UTC.

from datetime import datetime, date, timedelta, timezone

naive = datetime(2026, 9, 4, 14, 30, 0)
aware = datetime(2026, 9, 4, 14, 30, 0, tzinfo=timezone.utc)

print("naive:", naive, "| tzinfo:", naive.tzinfo)
print("aware:", aware, "| tzinfo:", aware.tzinfo)
print("iso  :", aware.isoformat())
print("parts:", aware.year, aware.month, aware.day, aware.hour)
print("weekday:", aware.strftime("%A"), "| month:", aware.strftime("%B"))
print("date only:", aware.date(), "| time only:", aware.time())

try:
    naive - aware
except TypeError as err:
    print("mixing naive and aware:", err)

Example

timedelta arithmetic

Durations add and subtract like numbers.

from datetime import datetime, timedelta, timezone

start = datetime(2026, 9, 1, 9, 0, tzinfo=timezone.utc)
end = datetime(2026, 9, 4, 17, 30, tzinfo=timezone.utc)

duration = end - start
print("duration       :", duration)
print("total hours    :", round(duration.total_seconds() / 3600, 2))
print("days component :", duration.days)

print("\nretention cutoff:", (end - timedelta(days=30)).date())
print("next run        :", (end + timedelta(hours=6)).isoformat())
print("timeout window  :", timedelta(seconds=90) > timedelta(minutes=1))

Example

Parsing and formatting log timestamps

strptime reads text; isoformat writes the machine-readable version.

from datetime import datetime, timezone

raw_lines = [
    "2026-09-04 10:22:31 INFO started",
    "2026-09-04 10:25:08 ERROR timeout",
]

events = []
for line in raw_lines:
    stamp_text = line[:19]
    stamp = datetime.strptime(stamp_text, "%Y-%m-%d %H:%M:%S").replace(tzinfo=timezone.utc)
    events.append((stamp, line[20:]))

for stamp, message in events:
    print(stamp.isoformat(), "|", message)

gap = events[1][0] - events[0][0]
print("\ntime to failure:", gap.total_seconds(), "seconds")
print("round trip:", datetime.fromisoformat(events[0][0].isoformat()) == events[0][0])
print("display format:", events[0][0].strftime("%d %b %Y, %I:%M %p UTC"))

Example

Group usage by day

The pattern behind every daily cost or traffic report.

from collections import defaultdict
from datetime import datetime, timezone

calls = [
    {"ts": "2026-09-02T10:00:00+00:00", "tokens": 500},
    {"ts": "2026-09-02T18:30:00+00:00", "tokens": 700},
    {"ts": "2026-09-03T09:15:00+00:00", "tokens": 1200},
    {"ts": "2026-09-04T11:45:00+00:00", "tokens": 300},
]

by_day = defaultdict(int)
for call in calls:
    day = datetime.fromisoformat(call["ts"]).astimezone(timezone.utc).date()
    by_day[day] += call["tokens"]

for day in sorted(by_day):
    bar = "#" * (by_day[day] // 100)
    print(f"{day}  {by_day[day]:>5} tokens  {bar}")

print("\ntotal:", sum(by_day.values()))

Example

Measure durations with perf_counter

Monotonic clock — immune to system clock changes.

import time
from datetime import datetime, timezone

start = time.perf_counter()
total = sum(i * i for i in range(200_000))
elapsed_ms = (time.perf_counter() - start) * 1000

print(f"computed {total} in {elapsed_ms:.2f} ms")
print("logged at:", datetime.now(timezone.utc).isoformat(timespec="seconds"))
print("filename-safe stamp:", datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S"))

Takeaways

  • Store UTC with aware datetimes; convert to local only for display.
  • isoformat/fromisoformat for machine data, strftime/strptime for human formats.
  • Measure elapsed time with time.perf_counter, not datetime subtraction.