Dates and times, and the hour that does not exist
The three types
from datetime import date, datetime, timedelta
date(2026, 9, 4) # a day, no time
datetime(2026, 9, 4, 14, 30) # a day and a time
timedelta(days=7, hours=3) # a durationArithmetic works between them:
d = date.today()
d + timedelta(days=30) # a date 30 days later
(datetime.now() - started).total_seconds()total_seconds() is the one to remember. timedelta.seconds is the seconds component and excludes the days, so a two-day duration reports 0 seconds. This is one of the most reliable sources of wrong elapsed-time calculations.
Aware and naive
datetime.now() # naive — no timezone attached
datetime.now(timezone.utc) # awareA naive datetime is a number with no reference point. It cannot be compared with an aware one — Python raises TypeError: can't compare offset-naive and offset-aware datetimes rather than guessing — and it cannot be converted, because nothing records what it meant.
The rule that avoids nearly all timezone bugs:
Store and compute in UTC. Convert to local only when displaying.
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
now = datetime.now(timezone.utc) # store this
local = now.astimezone(ZoneInfo("Asia/Kolkata")) # show thiszoneinfo is in the standard library from Python 3.9 and reads the operating system's timezone database. On Windows there is often no such database, and you install the free tzdata package to supply one — a common and confusing first failure.
Note the timezone is named Asia/Kolkata, not IST. Abbreviations are ambiguous: IST is India, Israel and Ireland depending on who is speaking. Always use the region name.
Why an offset is not a timezone
India is UTC+5:30, permanently. Most places are not that simple: their offset changes twice a year. Storing "+05:30" records what the offset was at one moment; storing Asia/Kolkata records the rules, including future changes.
Two failures follow from getting this wrong:
- The hour that does not exist. When clocks go forward, 02:30 local time simply did not occur. A naive datetime holding it cannot be converted to a real instant.
- The hour that happens twice. When clocks go back, 01:30 occurs twice, and a timestamp without an offset is ambiguous. Sorting events by local time then puts them in the wrong order.
India has no daylight saving, which makes this invisible for a while and then very visible the first time you handle data from a country that does. The half-hour offset creates its own surprise in the other direction: code elsewhere that assumes whole-hour offsets produces times half an hour out.
Parsing and formatting
datetime.fromisoformat("2026-09-04T14:30:00+05:30") # the good case
datetime.strptime("04/09/2026", "%d/%m/%Y") # a known format
d.strftime("%d %b %Y") # '04 Sep 2026'
d.isoformat() # '2026-09-04'The codes worth memorising: %Y four-digit year, %m month number, %d day, %H hour on 24, %M minute, %S second, %b short month name, %B full month name.
ISO 8601 — 2026-09-04 — is the format to store and exchange. It sorts correctly as text, it is unambiguous, and every language parses it. 04/09/2026 is 4 September in most of the world and 9 April in the United States, and no amount of care at your end fixes a file that mixes both. When you receive such a file, ask; do not infer from the rows where the day exceeds 12, because that only tells you about those rows.
strptime raises ValueError on anything that does not match exactly, which is the behaviour you want at a boundary. The free dateutil package guesses formats and is useful for messy input, at the cost of guessing.
Epoch seconds
ts = now.timestamp() # seconds since 1970-01-01 UTC
datetime.fromtimestamp(ts, tz=timezone.utc)Epoch time is unambiguous and compact, which is why logs and APIs use it. Two cautions: fromtimestamp without tz gives you a naive local datetime, quietly reintroducing the problem; and some systems send milliseconds rather than seconds, which produces dates tens of thousands of years in the future if you do not divide by 1000.
What is deprecated
datetime.utcnow() returns a naive datetime that claims to be UTC while carrying no timezone, which combines the worst of both. It is deprecated from Python 3.12. Use datetime.now(timezone.utc).
Measuring elapsed time
For timing an operation, do not subtract wall-clock datetimes — the system clock can be adjusted mid-measurement, and has been known to produce negative durations.
import time
start = time.perf_counter()
do_work()
print(f"{time.perf_counter() - start:.3f}s")perf_counter is monotonic: it only moves forward, and is unaffected by clock changes. Use datetime for when, perf_counter for how long.
The one thing to keep
Store instants in UTC as aware datetimes and convert with a named zone only for display, and use `perf_counter` rather than clock arithmetic to measure how long something took.
Before you move on
A logging system records `datetime.now()` on servers in Mumbai and Frankfurt and sorts the merged events by that field. Ordering is wrong by hours. What is the underlying defect?
Pick the one you would defend. Nobody sees your answer.