Filter concepts by levelShowing all levels.

Python · Date and Time

Date and Time

The datetime/zoneinfo semantics behind most time-related production bugs — verified against real DST transitions

  • date · time · datetime · timedeltafour types, and which arithmetic between them is legal
  • ZoneInfo, not a fixed offsetthe only kind that gets DST right for a recurring or future date
  • Naive vs. awarecomparing them raises TypeError — the classic datetime bug
  • fold=0 / fold=1disambiguates the wall-clock hour that a fall-back DST transition repeats

1.datetime, date, time, and timedelta

date holds a calendar day, time holds a clock reading with no day, datetime combines both into one point in time, and timedelta holds a duration you can add to or subtract from any of the other three.

Key features

  • date(year, month, day) has no clock time — comparing two dates ignores time of day entirely
  • time(hour, minute, second) has no calendar day — it cannot be placed on a timeline by itself
  • datetime(year, month, day, hour, minute, second) is the only one of the three that is a specific instant
  • timedelta represents a span, not a point — datetime + timedelta gives a datetime, but datetime + datetime is a TypeError
  • datetime.combine(a_date, a_time) builds a datetime out of a separate date and time value

The four datetime.* types — what each one holds

The four datetime.* types — what each one holds
TypeHoldsMissingdate2 - date1 gives
date(y, m, d)calendar dayno time of day, no time zonetimedelta (whole days)
time(h, mi, s)clock readingno calendar day at alltimedelta (same-day only, no tz math)
datetime(y, m, d, h, mi, s)a specific point in timetime zone, unless tzinfo= is passedtimedelta (exact duration)
timedelta(days=, hours=, ...)a duration/spanno start or end point of its ownn/a — it is already a difference
date + time vs. datetime

date and time, separately

  • date(2026, 8, 21) — a day, no clock reading
  • time(9, 30) — a clock reading, no day
  • Neither alone is a point on a timeline

datetime, combined

  • datetime(2026, 8, 21, 9, 30) — both at once
  • datetime.combine(d, t) builds one from the other two
  • Only this form is a specific instant you can order or diff precisely
  • date and time, separately
    • date(2026, 8, 21) — a day, no clock reading
    • time(9, 30) — a clock reading, no day
    • Neither alone is a point on a timeline
  • datetime, combined
    • datetime(2026, 8, 21, 9, 30) — both at once
    • datetime.combine(d, t) builds one from the other two
    • Only this form is a specific instant you can order or diff precisely

Command

python
from datetime import date, time, datetime, timedelta

date(2026, 8, 21)                      # calendar day only
time(9, 30)                            # clock reading only
datetime(2026, 8, 21, 9, 30)           # both
datetime.combine(date(2026, 8, 21), time(9, 30))
date(2026, 8, 21) + timedelta(days=10) # -> date(2026, 8, 31)

date is a day, time is a clock reading, datetime is both together as one instant, and timedelta is a duration you add to or subtract from any of the first three — but never mix a bare date with a datetime in arithmetic.

2.Time zones, UTC, and zoneinfo

UTC is the one time reference that never shifts for daylight saving; zoneinfo.ZoneInfo("Region/City") attaches a real, named time zone to a datetime, and .astimezone() converts one specific instant between zones correctly.

Key features

  • timezone.utc is a fixed, always-correct zero offset — it never has daylight saving to account for
  • ZoneInfo("Region/City") looks up the IANA time zone database, including its DST schedule
  • .astimezone(zone) converts an aware datetime to a different zone without changing the instant it represents
  • Calling .astimezone() on a naive datetime assumes the system local zone — attach a zone explicitly instead
  • Two datetimes with different tzinfo can still be equal, and can be compared, once both are aware

UTC and zoneinfo — the conversion surface

UTC and zoneinfo — the conversion surface
CallWhat it does
timezone.utca fixed, zero-DST offset — the reference every other zone is measured from
ZoneInfo("America/New_York")a real IANA zone; its offset depends on the specific date given
dt.astimezone(ZoneInfo(...))converts an aware datetime to another zone, same instant, different label
dt.tzname()the abbreviation in effect for that instant, e.g. 'EDT' or 'EST'
dt.utcoffset()the timedelta currently separating this zone from UTC

Command

python
from datetime import datetime, timezone
from zoneinfo import ZoneInfo

datetime.now(ZoneInfo("Asia/Tokyo"))
some_utc_dt.astimezone(ZoneInfo("Europe/London"))

UTC never shifts for DST; ZoneInfo("Region/City") does, correctly, because it looks up real DST rules — a fixed offset object does not, and drifts wrong for part of the year.

3.Naive vs. aware datetimes

A naive datetime has no time zone attached, so it does not represent one specific instant. An aware datetime has a tzinfo, so it does — and Python refuses to compare or subtract a naive one against an aware one.

Key features

  • dt.tzinfo is None on a naive datetime, and a tzinfo object on an aware one
  • datetime.now() returns naive; datetime.now(timezone.utc) returns aware
  • Comparing or subtracting naive and aware datetimes raises TypeError, not a wrong answer
  • .replace(tzinfo=zone) attaches a zone WITHOUT shifting the wall-clock numbers
  • .astimezone(zone) both attaches a zone AND shifts the wall-clock numbers to match it

Naive vs. aware — what changes

Naive vs. aware — what changes
PropertyNaiveAware
.tzinfoNonea tzinfo object (e.g. ZoneInfo(...), timezone.utc)
Represents one instant?No — ambiguous without external contextYes — unambiguous
datetime.now(...)datetime.now() — no argumentdatetime.now(timezone.utc) — zone passed
Comparable to the other kind?TypeError if compared to an aware datetimeTypeError if compared to a naive datetime
Attach a zone without shifting time.replace(tzinfo=zone)n/a — already has one
A naive datetime vs. an aware one

Naive — datetime(2026, 8, 21, 9, 0)

  • .tzinfo is None
  • Not one specific instant — 9:00 where?
  • Cannot be compared to an aware datetime

Aware — datetime(2026, 8, 21, 9, 0, tzinfo=timezone.utc)

  • .tzinfo is a real zone object
  • One unambiguous point on the timeline
  • Safe to compare, subtract, and convert with .astimezone()
  • Naive — datetime(2026, 8, 21, 9, 0)
    • .tzinfo is None
    • Not one specific instant — 9:00 where?
    • Cannot be compared to an aware datetime
  • Aware — datetime(2026, 8, 21, 9, 0, tzinfo=timezone.utc)
    • .tzinfo is a real zone object
    • One unambiguous point on the timeline
    • Safe to compare, subtract, and convert with .astimezone()

Command

python
from datetime import datetime, timezone
from zoneinfo import ZoneInfo

datetime.now()                        # naive
datetime.now(timezone.utc)            # aware
naive_dt.replace(tzinfo=ZoneInfo("America/Chicago"))  # attach, no shift
aware_dt.astimezone(timezone.utc)     # convert, values shift

A naive datetime has no tzinfo and does not pin down one instant; call datetime.now(timezone.utc), never bare datetime.now(), and use .replace(tzinfo=...) to label a value versus .astimezone() to convert it.

4.ISO 8601, Unix timestamps, and date serialization

ISO 8601 ("2026-08-21T14:30:00+00:00") is the standard human-readable text format for a datetime; a Unix timestamp is the same instant as a single number — seconds since 1970-01-01 UTC. Both round-trip through .isoformat()/.fromisoformat() and .timestamp()/.fromtimestamp().

Key features

  • .isoformat() produces "YYYY-MM-DDTHH:MM:SS±HH:MM"; datetime.fromisoformat() parses it back
  • A Unix timestamp is a float — seconds (and fractions of a second) since 1970-01-01T00:00:00 UTC
  • .timestamp() converts an aware datetime to its Unix timestamp; datetime.fromtimestamp(ts, tz=...) converts back
  • JSON has no native datetime type — serialize with .isoformat() and store the result as a plain string
  • fromtimestamp(ts) without tz= assumes the system local zone; always pass tz=timezone.utc explicitly for a stored timestamp

ISO 8601 and Unix timestamps — converting both ways

ISO 8601 and Unix timestamps — converting both ways
DirectionCall
datetime → ISO 8601 stringdt.isoformat()
ISO 8601 string → datetimedatetime.fromisoformat(s)
aware datetime → Unix timestampdt.timestamp()
Unix timestamp → aware datetimedatetime.fromtimestamp(ts, tz=timezone.utc)
datetime → custom stringdt.strftime(fmt)

Command

python
from datetime import datetime, timezone

datetime.fromisoformat("2026-08-21")           # date-only ISO string works too
dt.timestamp()                                  # -> float, seconds since epoch
datetime.fromtimestamp(ts, tz=timezone.utc)     # always pass tz explicitly

Serialize a datetime as .isoformat() text (JSON has no datetime type) or .timestamp() as a number; when parsing a timestamp back, always pass tz=timezone.utc — fromtimestamp() without it silently uses the local machine's zone.

5.Daylight saving time transitions

A DST transition makes one wall-clock hour happen twice ("fall back", ambiguous) or skips it entirely ("spring forward", imaginary). The fold attribute on a datetime picks which of the two repeated occurrences is meant.

Key features

  • fold=0 (default) means the earlier of two identical wall-clock readings; fold=1 means the later
  • Fall-back creates an ambiguous hour — the same wall-clock time occurs twice, an hour apart in UTC
  • Spring-forward creates an imaginary hour — that wall-clock time never actually occurs
  • zoneinfo.ZoneInfo respects fold correctly; the legacy datetime.timezone (fixed offset) ignores it entirely
  • Two datetimes that differ only by fold can compare equal as wall-clock values but convert to different UTC instants

DST transitions in America/New_York, 2026 — real, computed dates

DST transitions in America/New_York, 2026 — real, computed dates
TransitionDateWall clock doesfold behavior
Spring forward2026-03-08, 2:00 AMjumps to 3:00 AM — 2:00-2:59 AM never happensfold ignored; the reading is imaginary either way
Fall back2026-11-01, 2:00 AMresets to 1:00 AM — 1:00-1:59 AM happens twicefold=0 is the first pass (EDT), fold=1 is the second (EST)
America/New_York, November 1, 2026 — the fall-back hour repeats
  1. 1:00 AM EDT

    First 1:00 AM

    fold=0 begins — UTC-4

  2. 1:59 AM EDT

    Clock about to reset

    still fold=0, still UTC-4

  3. 1:00 AM EST

    Clock resets to 1:00 AM again

    fold=1 begins — UTC-5

  4. 1:59 AM EST

    Second 1:59 AM

    fold=1 ends; 2:00 AM EST follows normally

  1. 1:00 AM EDT: First 1:00 AM — fold=0 begins — UTC-4
  2. 1:59 AM EDT: Clock about to reset — still fold=0, still UTC-4
  3. 1:00 AM EST: Clock resets to 1:00 AM again — fold=1 begins — UTC-5
  4. 1:59 AM EST: Second 1:59 AM — fold=1 ends; 2:00 AM EST follows normally

Command

python
from datetime import datetime
from zoneinfo import ZoneInfo

tz = ZoneInfo("America/New_York")
datetime(2026, 11, 1, 1, 30, fold=0, tzinfo=tz)   # first 1:30 AM (EDT)
datetime(2026, 11, 1, 1, 30, fold=1, tzinfo=tz)   # second 1:30 AM (EST)

Fall-back repeats an hour (disambiguate with fold=0/fold=1); spring-forward skips an hour entirely (that reading is imaginary, even though Python will still construct it) — and timedelta(days=1) across either boundary is not always 24 real hours.

Remember

  • date is a day, time is a clock reading, datetime is both together — never mix a bare date with a datetime in arithmetic.
  • ZoneInfo("Region/City") tracks real DST rules; a fixed-offset timezone does not — use ZoneInfo for anything recurring or future-dated.
  • Always call datetime.now(timezone.utc), never bare datetime.now() — mixing naive and aware datetimes raises TypeError the moment they meet.
  • Serialize with .isoformat() (JSON has no datetime type); parse a stored Unix timestamp with fromtimestamp(ts, tz=timezone.utc), never bare.

Pro tip

  • fold=0 is the earlier of a repeated fall-back hour, fold=1 the later — the legacy fixed-offset timezone class ignores fold entirely.
  • timedelta(days=1) across a DST boundary preserves the wall-clock reading, not the elapsed real time — a "run every 24 hours" job can drift by an hour twice a year.
  • A datetime built inside a spring-forward gap (e.g. 2:30 AM on a transition day) is never rejected — it silently gets the pre-transition offset for a reading the clock never showed.
  • Use .replace(tzinfo=zone) to LABEL a naive value that is already correct for that zone; use .astimezone(zone) to CONVERT an aware value to a different zone.

Elsewhere in this course

  • datetime and zoneinfo basics — see Standard Library § datetime and zoneinfo

Sources

  • The datetime module — docs.python.org/3/library/datetime.html
  • The zoneinfo module — docs.python.org/3/library/zoneinfo.html
  • Every example executed on Python 3.14.3 · checked 2026-08-22

Spotted a mistake on this page? Tell us.