skip to content

Dates, Times, and Time Zones

Where correct-looking code goes wrong an hour twice a year: the datetime types and the naive-versus-aware split, zoneinfo, timedelta arithmetic and parsing, and monotonic clocks for elapsed time.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

19

Why measure elapsed time with time.monotonic() instead of time.time() in Python?

level: juniorimportance: must knowfreq 60%

answer

  1. Two clocks answer two different questions
  2. One of them can jump backwards
  3. NTP steps, snapshots and manual date changes
  4. Durations versus naming an instant
  5. monotonic() has no epoch at all

basics

~20 s

time.monotonic() only ever moves forward, so the difference between two readings is a true duration. time.time() names a wall-clock instant, and NTP or an operator can step it forwards or backwards, corrupting any elapsed-time calculation.

solid answer

~50 s

Python's `time` module exposes two different clocks because they answer two different questions. `time.time()` answers *what instant is it* — seconds since the Unix epoch, which you can turn into a calendar date, write into a log or compare with a timestamp from another host. `time.monotonic()` answers *how much time has passed* — seconds counted from an unspecified reference point that never decreases. The wall clock is settable: an NTP correction can step it, a virtual machine resumed from a snapshot inherits a jump, and an operator can set it by hand, so `end - start` over `time.time()` can come out negative or hours long. `time.monotonic()` cannot jump, so it is the right basis for timeouts, retry backoff and rate limits. `time.perf_counter()` is also monotonic and offers the finest resolution the platform has, so it is the one to reach for on short measurements.

code

python · 8 lines
python
import time

start = time.monotonic()
time.sleep(0.05)
print(f"elapsed {time.monotonic() - start:.3f}s")

print(time.get_clock_info("monotonic").adjustable)  # False
print(time.get_clock_info("time").adjustable)       # True

go deeper

for a junior

Recall the one-line rule: durations come from time.monotonic(), an instant comes from time.time(). Be able to name at least one reason the wall clock can move, such as an NTP correction.

for a middle

Explain the mechanics: what settable means, that time.get_clock_info() reports adjustable, and which everyday operations belong on which clock — timeouts and backoff on monotonic, log timestamps and stored expiry on the wall clock.

for a senior

Show you have debugged this. Be ready to describe a negative duration or a timeout that fired an hour late, and to say what monotonic still does not guarantee: an undefined epoch, rate slewing, and platform-dependent behaviour across suspend.

for a principal

Own it as a convention rather than a habit. Decide where in a codebase clock reads are allowed at all, how time is injected so it can be faked in tests, and what the standard is for durations recorded in metrics versus timestamps recorded in logs.

## Two clocks because there are two questions Operating systems keep more than one notion of time, and Python surfaces them as separate functions in the `time` module rather than trying to paper over the difference. **The wall clock** — `time.time()`, and its integer sibling `time.time_ns()` — returns seconds since the Unix epoch (1970-01-01 UTC). Its value *means* something on its own: it names an instant, it can be rendered as a calendar date, and two machines that both read it are talking about the same moment. That is exactly what a log record, a token expiry or a row's `created_at` needs. **The monotonic clock** — `time.monotonic()`, `time.monotonic_ns()` and `time.perf_counter()` — returns seconds counted from a reference point the platform does not define. Its absolute value means nothing. The only meaningful operation is subtracting two readings taken in the same process on the same machine, and that difference is a real elapsed duration. ## Why the wall clock lies about durations The system clock is *settable*, and several ordinary things set it: * **NTP steps.** A time daemon that finds a large offset does not gently correct it; past a threshold it calls the "set the clock now" syscall and the clock jumps, possibly backwards. * **Boot.** A machine with a bad or missing hardware clock starts near the epoch and is corrected seconds later, which is a huge forward jump right when your service is starting up. * **Virtualization.** A guest resumed from a snapshot or migrated between hosts sees its clock corrected on the way in. * **Humans and containers.** Someone sets the date by hand, or a container inherits a host adjustment. Note what is *not* on that list: daylight saving. `time.time()` is UTC-based, so a DST transition never moves it — that is a separate concern belonging to the timezone-aware side of `datetime`. The failure modes are all quiet. A duration comes out negative and gets logged as a nonsense metric. A timeout computed as `time.time() + 30` fires instantly, or an hour late. A rate limiter that stores wall-clock timestamps opens the floodgates for an hour after the clock steps backwards. In one common shape — a batch tool that times each of, say, a 340-case regression pack with `time.time()` differences — a single NTP step mid-run poisons one case's timing, and because the totals are floats the bad value quietly drags the aggregate report off without raising anything. ## What monotonic guarantees, and what it does not Guaranteed: the value never decreases, and it is not affected by the clock being *set*. `time.get_clock_info("monotonic").adjustable` is `False`, while `time.get_clock_info("time").adjustable` is `True` — the standard library will tell you which is which at runtime. Not guaranteed: * **A stable rate.** A time daemon that corrects a small offset by *slewing* changes the clock's rate rather than stepping it, and on Linux the monotonic clock is slewed too. It never jumps, but it may run a fraction of a percent fast or slow while a correction is being absorbed. That is irrelevant for a timeout and relevant for a precision measurement. * **A meaningful epoch.** The reference point differs between platforms and between boots, so a monotonic reading must never be persisted, logged as a timestamp, or shipped to another service. * **Counting through suspend.** Whether the monotonic clock advances while the machine is asleep is platform-dependent. A laptop closed for an hour can wake with a "5 second" timeout that has not expired. ## Where perf_counter fits `time.perf_counter()` is documented as the clock with the highest available resolution for measuring a short interval, and it includes time spent asleep. On several platforms it and `time.monotonic()` read the same underlying counter; the distinction is one of contract, not always of mechanism. Use `perf_counter` when you are measuring something small, `monotonic` when you are enforcing a deadline. If you want CPU time rather than elapsed time — excluding sleeping and blocking — that is `time.process_time()` or `time.thread_time()`, which is a different question again. ## The rule to state in an interview Durations, timeouts, retry backoff, rate limiting, in-process cache TTLs: monotonic. Anything that names an instant and leaves the process — log timestamps, expiry stored in a database, a value compared with another machine's clock: the wall clock, stored as UTC. Mixing them is the bug; picking per question is the fix.

  • What does the absolute number returned by time.monotonic() actually represent?
    Nothing you can rely on. The reference point is deliberately unspecified and differs between platforms and between boots, so only the difference of two readings taken in the same process is meaningful. Never persist a monotonic reading, never log it as a timestamp, and never send it to another service expecting it to be comparable there.
  • Does time.monotonic() keep advancing while the machine is suspended?
    Not guaranteed, and it varies by platform. On Linux CPython reads the kernel's monotonic clock, which does not advance across a suspend, so a laptop closed for an hour can wake with a monotonic-based deadline that has barely moved. If suspended time must count toward a deadline, you need a wall-clock cross-check alongside the monotonic one.
  • Is time.monotonic() completely immune to NTP?
    It is immune to clock *steps* — it never jumps and never goes backwards. It is not immune to *slewing*: when a time daemon corrects a small offset by nudging the clock's rate, the monotonic clock's rate is nudged too on platforms like Linux. That is harmless for timeouts and worth knowing for sub-microsecond measurement.

A wall clock tells you what time the meeting starts; a stopwatch tells you how long the meeting ran. Resetting the wall clock mid-meeting does not change the stopwatch.

saying these in an interview costs you the question

  • Claims the system wall clock never moves backwards
  • Uses time.time() differences to enforce a timeout
  • Thinks time.monotonic() returns seconds since the Unix epoch
  • Believes NTP only ever slews and never steps the clock
  • Logs or persists a monotonic reading as a timestamp
  • Says a DST transition changes the value of time.time()

context

open as a page

What makes a Python datetime object aware rather than naive?

level: juniorimportance: must knowfreq 70%

basics

~10 s

A datetime is aware when it carries a tzinfo whose utcoffset() returns a real offset; otherwise it is naive. datetime.datetime(2026, 9, 5, 12, 0) is naive, and passing tzinfo=datetime.timezone.utc makes the same reading aware.

open as a page

Why use zoneinfo.ZoneInfo('Europe/Berlin') instead of a fixed UTC+1 offset?

level: juniorimportance: must knowfreq 52%

basics

~20 s

ZoneInfo('Europe/Berlin') carries that region's full IANA rule history, so the offset it reports depends on the moment in question. A datetime.timezone built from a timedelta is one frozen number and is wrong for part of every year.

open as a page

What does the fold attribute on a datetime.datetime mean during a DST transition?

level: middleimportance: must knowfreq 45%

basics

~20 s

fold is a 0-or-1 flag that says which pass through a repeated local wall time you mean: 0 the first occurrence, 1 the second. It defaults to 0 and only changes anything across an offset change.

open as a page

Why does comparing a naive and an aware Python datetime raise TypeError?

level: middleimportance: must knowfreq 60%

basics

~20 s

A naive datetime states no offset, so there is no shared instant to order against an aware one, and Python refuses to guess. Ordering and subtraction raise TypeError; equality does not raise — it quietly returns False.

open as a page

Why does timedelta.seconds mislead after subtracting two datetime objects, and what should you use instead?

level: middleimportance: must knowfreq 60%

basics

~20 s

Subtracting two datetime objects returns a timedelta normalised into days, seconds and microseconds. The seconds attribute is only the sub-day remainder, 0 to 86399, so it hides whole days and misreads negatives. Call total_seconds() for the whole duration.

open as a page

How do datetime.fromisoformat() and datetime.strptime() differ when parsing a timestamp string?

level: juniorimportance: should knowfreq 50%

basics

~20 s

fromisoformat parses ISO 8601 text with no format string, in C, and never touches the locale. strptime takes an explicit directive format so it reads any layout, but it is slower and locale-sensitive for month and weekday names.

open as a page

Why does a datetime for 02:30 on a spring-forward date not raise in Python?

level: middleimportance: should knowfreq 32%

basics

~10 s

Nothing validates a local time against the zone's transitions. The constructor only range-checks the fields and stores the tzinfo, so an hour the clock skipped builds fine and misbehaves later, when something converts it.

open as a page

When does time.perf_counter_ns() beat time.perf_counter() for timing Python code?

level: middleimportance: should knowfreq 32%

basics

~20 s

time.perf_counter() returns float seconds, and a float carries only 53 bits of mantissa, so a large reading cannot resolve nanoseconds. time.perf_counter_ns() returns an exact integer count of nanoseconds, so no precision is lost to rounding.

open as a page

Why is datetime.datetime.utcnow() deprecated, and what replaces it?

level: middleimportance: should knowfreq 55%

basics

~10 s

It returned UTC wall-clock fields with tzinfo left as None, so the value looked tagged but was naive. Python 3.12 deprecated it; use datetime.datetime.now(datetime.timezone.utc), which returns an aware object.

open as a page

Why does datetime.timedelta accept no months or years argument, and how do you shift a date by one month?

level: middleimportance: should knowfreq 35%

basics

~20 s

timedelta models an exact elapsed duration, and months and years have no fixed length - 28 to 31 days, 365 or 366. Calendar shifts are done by rebuilding the date with replace(), clamping the day using calendar.monthrange().

open as a page

How do datetime.astimezone(tz) and datetime.replace(tzinfo=tz) differ?

level: middleimportance: should knowfreq 55%

basics

~20 s

astimezone converts: it keeps the same absolute instant and recomputes the wall-clock fields for the target zone. replace(tzinfo=...) relabels: it keeps the digits and swaps the zone, which moves the instant. Neither raises, so the wrong one is a silent bug.

open as a page

Where does zoneinfo.ZoneInfo load its IANA time-zone data from at runtime?

level: middleimportance: should knowfreq 36%

basics

~10 s

It searches the directories in zoneinfo.TZPATH, which default to the system tz database, and falls back to the first-party tzdata distribution from PyPI if it is installed. With neither present, a lookup raises zoneinfo.ZoneInfoNotFoundError.

open as a page

How do you schedule a nightly Python job across DST so it neither repeats nor skips?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Decide whether the job is anchored to an absolute cadence or to a local wall clock. Absolute means a stored UTC instant plus a fixed interval; wall-clock means recomputing the local time each night and resolving gaps and repeats explicitly.

open as a page

How should a threading.Condition wait recompute its timeout after an early wake-up?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Compute an absolute deadline once as time.monotonic() plus the timeout, then on each pass pass remaining = deadline - time.monotonic() to the wait and give up when it drops to zero or below. Re-passing the original timeout makes the total wait unbounded.

open as a page

Your invoice-PDF renderer stores naive datetimes; how do you move it to aware timestamps safely?

level: seniorimportance: should knowfreq 40%

basics

~20 s

First establish what the stored naive values meant — UTC fields or the writer machine's local time — because that decides whether the backfill tags or shifts. Then normalise every datetime to aware UTC at one boundary and reject naive input there.

open as a page

Why can datetime.timestamp() shift a stored epoch by hours in a translation-memory updater's sync window?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Called on a datetime whose tzinfo is None, timestamp() assumes the value is local time on the host and applies that host's UTC offset. Change the host or its TZ setting and the same wall-clock value maps to a different epoch.

open as a page

How do you store UTC and render local time in a multi-site lab data loader?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Normalize every incoming reading to an aware UTC datetime at the boundary, persist that instant plus the site's IANA key, and convert with astimezone(ZoneInfo(key)) only where a human reads the output. Nothing in between should carry a local wall clock.

open as a page

When should you store a future event as UTC instead of local time plus an IANA zone key?

level: principalimportance: should knowfreq 30%

basics

~20 s

Store UTC when the record answers 'what moment was that?' — anything already past. Store local wall time plus the IANA key when it answers 'what wall clock did we promise?', because zone rules change under it.

open as a page