The datetime module provides three core types. date holds a calendar date
(year, month, day) with no time. time holds a time of day (hour, minute,
second, microsecond) with no date. datetime combines both into a
single timestamp.
from datetime import date, time, datetime
d = date(2026, 6, 18) # just the date
t = time(14, 30, 0) # just the time of day
dt = datetime(2026, 6, 18, 14, 30) # date + time together
dt.date() # -> date(2026, 6, 18)
dt.time() # -> time(14, 30)
date.today() # current date
Use date for things like birthdays or due dates where time is irrelevant, time
for a recurring clock time, and datetime for actual events/timestamps. Most
real-world work uses datetime.
A naive datetime has no timezone info (tzinfo is None) — it's just wall
clock numbers with no reference point, so it's ambiguous. An aware datetime
carries a tzinfo, pinning it to an actual instant. Use the stdlib zoneinfo
module (Python 3.9+) to attach real IANA timezones.
from datetime import datetime
from zoneinfo import ZoneInfo
naive = datetime(2026, 6, 18, 14, 30) # ambiguous — no tz
aware = datetime(2026, 6, 18, 14, 30,
tzinfo=ZoneInfo("America/New_York"))
utc = aware.astimezone(ZoneInfo("UTC")) # convert between zones
You can't compare or subtract a naive and an aware datetime — it raises
TypeError. Best practice: store and compute in UTC-aware datetimes, and convert
to local zones only for display.
They are inverses. strftime ("string from time") formats a datetime
into a string using format codes. strptime ("string parse time")
parses a string into a datetime using a matching format.
from datetime import datetime
dt = datetime(2026, 6, 18, 14, 30)
s = dt.strftime("%Y-%m-%d %H:%M") # datetime -> "2026-06-18 14:30"
parsed = datetime.strptime("2026-06-18 14:30",
"%Y-%m-%d %H:%M") # str -> datetime
Common codes: %Y (4-digit year), %m (month), %d (day), %H (24-hour),
%M (minute), %S (second). To remember: f = format (out), p = parse (in).
For standard ISO strings, datetime.fromisoformat() / .isoformat() are simpler.
A timedelta represents a duration — a difference between two points in time.
Subtracting two datetimes yields a timedelta; adding a timedelta to a datetime
shifts it. A timedelta stores days, seconds, and microseconds.
from datetime import datetime, timedelta
start = datetime(2026, 6, 18, 9, 0)
end = datetime(2026, 6, 18, 17, 30)
worked = end - start # timedelta(seconds=30600)
worked.total_seconds() # 30600.0
worked.seconds // 3600 # 8 (hours portion)
tomorrow = start + timedelta(days=1) # shift forward
week_ago = start - timedelta(weeks=1) # shift back
Use total_seconds() to get the whole duration as a number (the .seconds
attribute is only the sub-day part). timedelta makes date math safe — it correctly
rolls over months and years.
The big trap: both datetime.now() and the old datetime.utcnow() return naive
datetimes. now() gives local wall time, utcnow() gives the UTC wall time — but
neither attaches a tzinfo, so a utcnow() value silently looks like local
time and corrupts later conversions. utcnow() is deprecated in modern Python.
from datetime import datetime
from zoneinfo import ZoneInfo
datetime.now() # naive, local time — ambiguous
datetime.utcnow() # naive, but labelled nothing! (deprecated)
# correct: an AWARE UTC timestamp
now_utc = datetime.now(ZoneInfo("UTC"))
local = datetime.now(ZoneInfo("America/New_York"))
Rule of thumb: always pass a timezone to now() to get an aware datetime, and
avoid utcnow() entirely. Store timestamps as UTC-aware and convert for display.
Use datetime.fromisoformat() to parse and .isoformat() to produce ISO 8601
strings — simpler and faster than strptime/strftime for the standard format.
Python 3.11+ parses a much wider range (including Z suffix).
from datetime import datetime
dt = datetime.fromisoformat("2026-06-18T14:30:00+00:00")
dt.isoformat() # '2026-06-18T14:30:00+00:00'
datetime.fromisoformat("2026-06-18T14:30:00Z") # 3.11+ accepts 'Z'
Rule of thumb: prefer fromisoformat/isoformat for ISO strings (APIs, JSON,
databases); reserve strptime/strftime for non-standard custom formats.
.timestamp() converts an aware datetime to a Unix epoch float (seconds since
1970 UTC); datetime.fromtimestamp(ts, tz) converts back. For naive datetimes,
.timestamp() assumes local time — another reason to stay aware.
from datetime import datetime
from zoneinfo import ZoneInfo
dt = datetime(2026, 6, 18, 14, 30, tzinfo=ZoneInfo("UTC"))
ts = dt.timestamp() # 1781879400.0
datetime.fromtimestamp(ts, ZoneInfo("UTC")) # back to the datetime
Rule of thumb: always pass a tz to fromtimestamp and use aware datetimes with
.timestamp() to avoid silent local-time assumptions.
zoneinfo applies the correct UTC offset for the date, so DST is handled
automatically — but arithmetic in local time can land on ambiguous (fall-back) or
nonexistent (spring-forward) times. Do math in UTC, then convert.
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
ny = ZoneInfo("America/New_York")
before = datetime(2026, 3, 8, 1, 30, tzinfo=ny) # just before spring-forward
# adding 1 hour in local time may skip the 2 AM that doesn't exist
utc = before.astimezone(ZoneInfo("UTC")) + timedelta(hours=1)
utc.astimezone(ny) # correct wall time
Rule of thumb: store/compute in UTC and convert to local only for display — never do duration math directly on DST-affected local datetimes.
Memorize the everyday codes: %Y/%y (4-/2-digit year), %m month, %d day, %H
24-hour / %I 12-hour, %M minute, %S second, %p AM/PM, %A/%a weekday name,
%B/%b month name, %z UTC offset, %j day-of-year.
from datetime import datetime
dt = datetime(2026, 6, 18, 14, 30)
dt.strftime("%A, %B %d, %Y") # 'Thursday, June 18, 2026'
dt.strftime("%I:%M %p") # '02:30 PM'
datetime.strptime("06/18/26", "%m/%d/%y")
Rule of thumb: %Y-%m-%d %H:%M:%S covers most needs; case matters (%M minute vs
%m month, %H 24h vs %I 12h).
date/time/datetime are immutable (and hashable, so usable as dict keys). To
get a changed version, use .replace(), which returns a new object with the
given fields swapped.
from datetime import datetime
dt = datetime(2026, 6, 18, 14, 30)
midnight = dt.replace(hour=0, minute=0) # new datetime
dt # unchanged
# attach a timezone to a naive datetime:
from zoneinfo import ZoneInfo
aware = dt.replace(tzinfo=ZoneInfo("UTC"))
Rule of thumb: datetimes never mutate — use .replace() for tweaks and timedelta
arithmetic for shifts; note .replace(tzinfo=...) labels without converting.
Use datetime.now(timezone.utc) (or ZoneInfo("UTC")) to get an aware UTC
timestamp. utcnow()/utcfromtimestamp() are deprecated in 3.12+ precisely because
they return naive values that misrepresent UTC as local.
from datetime import datetime, timezone
datetime.now(timezone.utc) # aware UTC — recommended
# NOT: datetime.utcnow() # deprecated, returns naive
Rule of thumb: replace every utcnow() with now(timezone.utc) to get a correct,
tz-aware UTC timestamp.
Datetimes support the comparison operators and sort chronologically, so sorted(),
min, and max work directly. The only rule: you cannot compare naive with
aware datetimes — it raises TypeError.
from datetime import datetime
events = [datetime(2026, 6, 18), datetime(2026, 1, 1), datetime(2026, 12, 31)]
sorted(events) # chronological order
max(events) # latest
# datetime(2026,1,1) < datetime(2026,1,1, tzinfo=utc) # TypeError!
Rule of thumb: keep all datetimes consistently naive or consistently aware before comparing/sorting — mixing the two errors out.
Use the lower-level time module for epoch seconds, sleeping, and
performance measurement. time.time() gives wall-clock epoch; time.perf_counter()
and time.monotonic() are for measuring durations (immune to clock changes).
import time
start = time.perf_counter()
time.sleep(0.1)
elapsed = time.perf_counter() - start # high-resolution duration
time.time() # Unix epoch seconds (wall clock)
time.monotonic() # never goes backwards — good for timeouts
Rule of thumb: datetime for calendar dates/times; time.perf_counter/monotonic
for measuring elapsed time, and time.sleep to pause.
date.weekday() returns 0=Monday..6=Sunday (isoweekday() is 1=Monday..7=Sunday).
The calendar module gives month/year grids, leap-year checks, and month lengths.
from datetime import date
import calendar
d = date(2026, 6, 18)
d.weekday() # 3 (Thursday)
d.isoweekday() # 4
calendar.isleap(2024) # True
calendar.monthrange(2026, 6) # (0, 30) -> (first weekday, days in month)
Rule of thumb: weekday()/isoweekday() for day-of-week logic; reach for the
calendar module for month lengths, leap years, and calendar layouts.
Either parse an offset-bearing ISO string with fromisoformat, or parse a naive
datetime and attach a zone with .replace(tzinfo=...) (label) or .astimezone()
(convert). strptime with %z reads explicit offsets.
from datetime import datetime
from zoneinfo import ZoneInfo
datetime.fromisoformat("2026-06-18T14:30+05:30") # aware directly
datetime.strptime("2026-06-18 +0530", "%Y-%m-%d %z") # %z reads the offset
naive = datetime(2026, 6, 18, 14, 30)
naive.replace(tzinfo=ZoneInfo("Asia/Kolkata")) # label as that zone
Rule of thumb: use %z/fromisoformat when the string carries an offset; use
.replace(tzinfo=...) to label a naive value as a known zone (no time shift).
More Standard Library Essentials interview questions
More ways to practice
The self-quiz is live. Join our channel for updates, new content & tech tips.