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 timestamptz column, 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 elsewhere

Two 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.

CodeMeaningExample
%Y %yyear 4-digit · 2-digit2026 · 26
%m %B %bmonth number · full name · abbrev09 · September · Sep
%d %eday of month, zero-padded · space-padded15
%A %aweekday full · abbrevTuesday · Tue
%H %I %phour 00-23 · hour 01-12 · AM/PM13 · 01 · PM
%M %S %fminute · second · microsecond (6 digits)45 · 30 · 000000
%z %:z %Zoffset +0000 · offset +00:00 (3.12+) · zone name+0000 · UTC
%jday of year 001-366258
%U %Wweek number, Sunday-first · Monday-first37
%G %V %uISO year · ISO week · ISO weekday 1-72026 · 38 · 2
%sUnix 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.

FunctionUseNotes
time.time()wall-clock epoch secondscan jump, not monotonic
time.monotonic()elapsed time, timeoutsnever goes backwards, arbitrary origin
time.perf_counter()benchmarkinghighest resolution, monotonic
time.process_time()CPU time of this processexcludes sleep and I/O
time.thread_time()CPU time of this thread—
*_ns variantsthe same in integer nanosecondsno 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.