datetime — and the one rule
Store and compute in UTC, convert only for display. A naive datetime — one with no timezone — is a bug that waits for a daylight-saving change or a server in another country, and then produces wrong answers instead of errors.
ISO 8601 (isoformat) is the interchange format: it sorts correctly as text, and every language reads it.
from datetime import datetime, date, timedelta, timezone
datetime.now(timezone.utc) # aware — always do this
datetime.now() # naive: no timezone, a future bug
datetime.now(timezone.utc) + timedelta(days=7, hours=3)
d = date(2026, 9, 22)
d.isoformat() # '2026-09-22'
date.fromisoformat("2026-09-22")
(date(2026, 12, 25) - d).days # 94
datetime.strptime("22/09/2026", "%d/%m/%Y") # parse
d.strftime("%A, %d %B %Y") # format
re — patterns, used sparingly
Always use a raw string (r"...") so the backslashes reach the regex engine. And the standard advice holds: a regular expression is the right tool for a pattern, and the wrong tool for a format — use a parser for HTML, JSON, CSV and dates.
import re
re.search(r"(\d{4})-(\d{2})", text) # first match anywhere, or None
re.match(r"\d+", text) # only at the start
re.findall(r"\w+@\w+\.\w+", text) # every match, as a list
re.sub(r"\s+", " ", text).strip() # collapse whitespace
re.split(r"[;,]\s*", text)
m = re.search(r"(?P<year>\d{4})-(?P<month>\d{2})", text)
m.group("year"), m.start(), m.groups()
pattern = re.compile(r"...") # compile it if it is in a loop
collections
deque with a maxlen is a ring buffer in one line — the last N log lines, a sliding window — and it drops from the other end automatically.
from collections import Counter, defaultdict, deque, namedtuple, ChainMap
Counter(words).most_common(3)
defaultdict(list)
q = deque([1, 2, 3], maxlen=100)
q.appendleft(0) # O(1) at BOTH ends — a list is O(n) at the front
q.popleft()
q.rotate(1)
ChainMap(overrides, defaults) # look up in order, without copying
random, and being able to reproduce it
random is a Mersenne Twister: excellent for simulations, useless for secrets, because the sequence is predictable from its state. Tokens, passwords and keys come from secrets.
import random
random.seed(42) # the same sequence every time — do this in tests
random.random() # 0.0 … 1.0
random.randint(1, 6) # inclusive at both ends
random.choice(items)
random.sample(items, 3) # without replacement
random.shuffle(items) # in place, returns None
import secrets
secrets.token_urlsafe(32) # for anything security-related. NOT random.
The rest of the shelf
Before reaching for a dependency, check here. Every package you add is a version to track, a licence to honour and an attack surface — and a surprising number of them wrap something that was already installed.
| Module | For |
|---|---|
math / statistics | isclose, gcd, hypot; mean, median, stdev, quantiles |
itertools / functools | Lazy composition; cache, partial, reduce |
json / csv / sqlite3 | Data in and out — yes, a real database is in the standard library |
pathlib / os / shutil | Paths, environment, copying and moving |
textwrap / difflib | Wrapping and dedenting; diffs and get_close_matches |
enum / decimal / fractions | Named constants; money; exact ratios |
unittest / doctest | Tests, with nothing to install |
logging | Instead of print in anything that runs unattended |