Python Cheatsheet
Dates and Times
Use this Python reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
The Four Types
from datetime import date, time, datetime, timedelta, timezone, UTC date(2026, 9, 15) # calendar day, no time, no zone time(13, 45, 30) # wall clock, no day datetime(2026, 9, 15, 13, 45, 30) # both, naive by default timedelta(days=2, hours=3) # a duration, the only arithmetic type date.today() # 2026-09-15, local calendar day datetime.now() # naive local time, avoid this datetime.now(UTC) # aware UTC, the one to use (UTC alias 3.11+) datetime.now(timezone.utc) # same thing, works on every version datetime.combine(date.today(), time(9, 0)) dt.date(), dt.time(), dt.timetz() # pull the pieces back out
All four are immutable and hashable, so use .replace(...) to derive a new value.
datetime subclasses date, which is why isinstance(dt, date) is True and why
you cannot compare a date to a datetime.
Aware vs Naive
A datetime is aware if tzinfo is set and utcoffset() is not None,
otherwise naive. Naive values carry no zone, so they cannot be compared with
aware ones and they silently lose meaning the moment they cross a machine or a DST
boundary.
datetime.now() - datetime.now(UTC) # TypeError: can't subtract offset-naive and offset-aware datetimes datetime.utcnow() # DEPRECATED in 3.12, it returns a NAIVE value that # merely happens to hold UTC. Never use it. datetime.now(UTC) # correct replacement datetime.fromtimestamp(ts, UTC) # instead of utcfromtimestamp
Store UTC, compute in UTC, convert to a local zone only for display. Keep every datetime aware from the moment it enters your program. Persist as an ISO 8601 string or a
timestamptzcolumn, never as a naive local timestamp.
# Attaching a zone to a naive value, two different operations naive = datetime(2026, 9, 15, 13, 45) naive.replace(tzinfo=UTC) # "this wall time WAS in UTC", no shift naive.astimezone(UTC) # "interpret as LOCAL, convert to UTC", shifts
zoneinfo and DST
zoneinfo (3.9+) reads the IANA database, from the OS or from the tzdata package
on Windows. Use region names, never a fixed offset, since only the name knows
about DST.
from zoneinfo import ZoneInfo, available_timezones ny = ZoneInfo("America/New_York") utc_now = datetime.now(UTC) utc_now.astimezone(ny) # same instant, New York wall clock datetime(2026, 9, 15, 9, 0, tzinfo=ny) # 9am New York, offset resolved for you sorted(available_timezones())[:3]
python -m pip install tzdata # required on Windows, harmless elsewhereTwo DST traps, both measured on America/New_York:
# 1. Spring forward. Adding a timedelta does WALL-CLOCK arithmetic. d = datetime(2026, 3, 8, 1, 30, tzinfo=ny) # 01:30 EST (-05:00) d + timedelta(hours=1) # 02:30-05:00, a time that # DOES NOT EXIST that day (d.astimezone(UTC) + timedelta(hours=1)).astimezone(ny) # 03:30-04:00, correct # 2. Fall back. 01:30 happens twice, `fold` picks which one. amb = datetime(2026, 11, 1, 1, 30, tzinfo=ny) amb.utcoffset() # -4:00, the first (DST) pass, fold=0 default amb.replace(fold=1).utcoffset() # -5:00, the second (standard) pass
Rule: do elapsed-time arithmetic in UTC, then convert back. Wall-clock
arithmetic ("same time tomorrow") is the only case where you add to a zoned value
and then re-normalize with .astimezone(tz).
strftime and strptime
strftime formats a datetime into a string, strptime parses a string into a
datetime. Same directive table both ways.
| Code | Meaning | Example |
|---|---|---|
%Y %y | year 4-digit · 2-digit | 2026 · 26 |
%m %B %b | month number · full name · abbrev | 09 · September · Sep |
%d %e | day of month, zero-padded · space-padded | 15 |
%A %a | weekday full · abbrev | Tuesday · Tue |
%H %I %p | hour 00-23 · hour 01-12 · AM/PM | 13 · 01 · PM |
%M %S %f | minute · second · microsecond (6 digits) | 45 · 30 · 000000 |
%z %:z %Z | offset +0000 · offset +00:00 (3.12+) · zone name | +0000 · UTC |
%j | day of year 001-366 | 258 |
%U %W | week number, Sunday-first · Monday-first | 37 |
%G %V %u | ISO year · ISO week · ISO weekday 1-7 | 2026 · 38 · 2 |
%s | Unix timestamp (platform specific, not portable) | — |
%% | a literal % | % |
dt = datetime(2026, 9, 15, 13, 45, 30, tzinfo=UTC) dt.strftime("%Y-%m-%d %H:%M:%S %Z") # '2026-09-15 13:45:30 UTC' dt.strftime("%A, %B %d, %Y at %I:%M %p") # 'Tuesday, September 15, 2026 at 01:45 PM' datetime.strptime("15/09/2026 1:45 PM", "%d/%m/%Y %I:%M %p") datetime.strptime("2026-09-15T13:45:30+0200", "%Y-%m-%dT%H:%M:%S%z") # aware
strptime raises ValueError: time data ... does not match format on the
smallest mismatch, and it is slow. %Z parses only a few English abbreviations,
and abbreviations are ambiguous anyway, so parse %z offsets instead. Locale
directives (%A, %B, %p) depend on LC_TIME, so never round-trip data
through them.
ISO 8601, the Fast Path
dt.isoformat() # '2026-09-15T13:45:30+00:00' dt.isoformat(sep=" ", timespec="seconds") # '2026-09-15 13:45:30+00:00' date.today().isoformat() # '2026-09-15' datetime.fromisoformat("2026-09-15T12:30:45.123456+02:00") datetime.fromisoformat("2026-09-15 12:30:45") # space separator is fine datetime.fromisoformat("20260915T123045Z") # 3.11+, basic format and Z date.fromisoformat("2026-09-15") time.fromisoformat("13:45:30")
Before 3.11, fromisoformat only accepted exactly what isoformat() produced, and
a trailing Z raised ValueError. On 3.11+ it handles nearly all of ISO 8601 and
is roughly an order of magnitude faster than strptime, so it is the right parser
for machine-generated timestamps. It still does not accept every exotic form (week
dates with a time, for example), so for messy human input reach for
dateutil.parser.parse.
timedelta Arithmetic
td = timedelta(days=2, hours=3, minutes=4) td.days, td.seconds, td.microseconds # 2, 11040, 0 (normalized, ONLY these three) td.total_seconds() # 183840.0 always use this for a duration tomorrow = datetime.now(UTC) + timedelta(days=1) gap = datetime(2026, 9, 15, tzinfo=UTC) - datetime(2026, 1, 1, tzinfo=UTC) gap.days # 257 td * 3, td / 2, td // 2, -td, abs(td) # scalar math works td1 / td2 # a float ratio timedelta(months=1) # TypeError: 'months' is an invalid keyword argument
There is no months or years because they are not fixed durations. Jan 31 plus
one month has no single answer. Do it with replace and calendar, or use
dateutil.
import calendar from dateutil.relativedelta import relativedelta # pip install python-dateutil d = date(2026, 1, 31) d + relativedelta(months=1) # 2026-02-28, clamps d + relativedelta(years=1, months=-2, day=1) # absolute and relative mix # Stdlib only: last day of a month, and next month y, m = 2026, 2 date(y, m, calendar.monthrange(y, m)[1]) # 2026-02-28 calendar.monthrange(2026, 2) # (6, 28) first weekday, length calendar.isleap(2024) # True
Comparing, Sorting, Truncating
d1 < d2, d1 == d2, min(dates), max(dates) # all natural, aware only vs aware sorted(events, key=lambda e: e.created_at) sorted(events, key=lambda e: e.created_at, reverse=True) dt.weekday() # Monday = 0 … Sunday = 6, use with calendar.day_name dt.isoweekday() # Monday = 1 … Sunday = 7, matches ISO and most databases dt.isocalendar() # IsoCalendarDate(year=2026, week=38, weekday=2) dt.toordinal(), date.fromordinal(739874) # 739874 is 2026-09-15 dt.timetuple() # Truncation, all with replace dt.replace(minute=0, second=0, microsecond=0) # hour boundary dt.replace(hour=0, minute=0, second=0, microsecond=0) # day boundary dt.replace(day=1, hour=0, minute=0, second=0, microsecond=0) # month boundary dt - timedelta(days=dt.weekday()) # back to Monday # Round down to a 15-minute bucket dt.replace(minute=dt.minute // 15 * 15, second=0, microsecond=0)
Timestamps
dt.timestamp() # 1789479930.0, seconds since the epoch, UTC datetime.fromtimestamp(ts, UTC) # aware UTC datetime.fromtimestamp(ts) # naive LOCAL, rarely what you want datetime.fromtimestamp(ts, ZoneInfo("Asia/Tokyo")) int(datetime.now(UTC).timestamp()) # integer epoch seconds ms = int(dt.timestamp() * 1000) # JavaScript-style milliseconds
Calling .timestamp() on a naive datetime assumes local time, which is a
common silent bug on a server whose TZ differs from your laptop.
Measuring Elapsed Time
Never subtract two time.time() calls to measure a duration. The system clock can
jump backwards from NTP or a manual change.
| Function | Use | Notes |
|---|---|---|
time.time() | wall-clock epoch seconds | can jump, not monotonic |
time.monotonic() | elapsed time, timeouts | never goes backwards, arbitrary origin |
time.perf_counter() | benchmarking | highest resolution, monotonic |
time.process_time() | CPU time of this process | excludes sleep and I/O |
time.thread_time() | CPU time of this thread | — |
*_ns variants | the same in integer nanoseconds | no float rounding |
import time start = time.perf_counter() do_work() elapsed = time.perf_counter() - start # seconds, as a float deadline = time.monotonic() + 30.0 while time.monotonic() < deadline: poll() import timeit timeit.timeit("sorted(data)", globals=globals(), number=1000)
python -m timeit -s "import json" "json.dumps({'a': 1})"
Beyond the Standard Library
python -m pip install python-dateutil # parsing anything, relativedelta, rrule python -m pip install pendulum # aware by default, nicer API, immutable python -m pip install whenever # typed aware/naive split, strict DST
from dateutil import parser, rrule parser.parse("Sep 15, 2026 1:45 PM") # fuzzy human input parser.isoparse("2026-09-15T13:45:30Z") # strict and fast list(rrule.rrule(rrule.WEEKLY, count=3, dtstart=datetime(2026, 9, 15))) import pendulum pendulum.now("America/New_York").add(months=1).to_iso8601_string()
Reach for dateutil when input is messy or you need recurrence rules. The
standard library plus zoneinfo covers everything else.