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.
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
- Store UTC, display local. A user in Hyderabad and one in London must see the same event at their own wall-clock time.
- Use aware datetimes at boundaries — anything entering a database or an API.
- Never do date arithmetic with
timedelta(days=30)and call it "a month". Months vary; usedateutil.relativedeltaif you need calendar months. - Measure durations with
time.perf_counter(), notdatetime.now()differences.perf_counteris monotonic and unaffected by clock adjustments. - Include a timezone in log timestamps, or a cross-region incident becomes unreconstructable.
Copy-paste examples
Copy into your own editor, or load one into the compiler below and press Run.
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)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))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"))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()))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"))Build a run manifest with timestamps
Try it — in-browser Python
Change the training duration and confirm the finish time and rate update.
Output
Python runs in your browser. First run downloads the runtime.
Press Run (or Ctrl+Enter) to execute.
CPython in WebAssembly. Stdlib works. NumPy, pandas, scikit-learn and Matplotlib load on demand, and charts render below. No input(), no GPU, no network installs.
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.