Part 5 · Advanced Python · Module 40
Standard Library Power Tools
"Batteries included" is Python's superpower, but only if you know which battery to grab.
Professionals reach for Counter instead of a counting loop, zoneinfo instead of
hand-rolled UTC offsets, a compiled verbose regex instead of a tangle of split calls, and
secrets instead of random for tokens. This module is a guided tour of the tools that
replace the most hand-written code, with the pitfalls that bite people in production.
- Pick the right container from
collectionsand composeitertoolsrecipes - Handle dates and times correctly: aware vs naive datetimes,
zoneinfo, and DST gaps and folds - Write readable, correct regular expressions with groups, named groups, lookarounds, flags, and
re.VERBOSE - Build CLIs with
argparse(including 3.14's color help andsuggest_on_error) - Use
sqlite3safely with parameters, rows, and transactions - Choose correctly between
uuid4and 3.14'suuid7,secretsandrandom, and hashing vs HMAC vs password hashing - Use the file utilities,
enum,textwrap, logging, and t-strings where they fit
collections: specialized containers
The collections module provides containers that encode common patterns. Each replaces a few lines
of fiddly code with one well-tested, usually C-implemented type.
| Type | Replaces | Key operations |
|---|---|---|
deque | A list used as a queue (pop(0) is O(n)) | O(1) append/appendleft/pop/popleft; maxlen for a ring buffer; rotate |
Counter | d[k] = d.get(k, 0) + 1 | most_common(n), update, arithmetic + - & |, total() |
defaultdict | d.setdefault(k, []).append(v) | Calls a factory for missing keys (list, set, int) |
ChainMap | Merging dicts for lookup | Searches several mappings in order without copying: CLI args → env → defaults |
namedtuple | Bare tuples with magic indexes | Named fields, immutable, tuple-compatible; _replace, _asdict (prefer typing.NamedTuple or a dataclass for new code) |
OrderedDict | (Mostly obsolete: dicts are ordered) | move_to_end, order-sensitive equality; handy for LRU caches |
itertools: lazy building blocks
itertools provides fast, memory-efficient iterator building blocks (M14
and M18 introduced the idea). Everything is lazy: nothing is computed until
you iterate. The docs' "recipes" section shows how to combine them; many recipes are now built-ins or available
in the third-party more-itertools package (pixi add more-itertools).
| Family | Tools |
|---|---|
| Infinite | count, cycle, repeat |
| Slicing and filtering | islice, takewhile, dropwhile, filterfalse, compress |
| Combining | chain, chain.from_iterable, zip_longest, pairwise (3.10), batched (3.12; strict= 3.13) |
| Grouping and reducing | groupby (consecutive keys!), accumulate, starmap, tee |
| Combinatorics | product, permutations, combinations, combinations_with_replacement |
Dates, times, and time zones
Time handling is where confident programmers write subtle bugs. The foundation is the difference between two
kinds of datetime:
Naive
tzinfo is None. A wall-clock reading with no zone: "9:00 on 2026-03-08",
but where? Comparing or subtracting naive datetimes from different places silently gives wrong answers.
datetime.now() and datetime.utcnow() (deprecated) return naive values.
Aware
Has a tzinfo, so it denotes an exact instant. Create with
datetime.now(UTC), datetime(..., tzinfo=ZoneInfo("Europe/Oslo")), or parse an ISO
string with an offset. Use aware datetimes for anything that crosses a process boundary.
The professional rule: store and compute in UTC, convert to local time only for display (and
keep the user's IANA zone name, like "America/New_York", not just an offset like
-05:00, since offsets change with DST and with legislation).
zoneinfo.ZoneInfo (3.9+) uses the IANA time-zone database: the operating system's copy, or the
tzdata package where the OS has none (Windows, minimal containers, and this browser).
DST pitfalls: gaps, folds, and wall-clock arithmetic
Daylight saving time creates two kinds of trouble:
- Gaps: when clocks spring forward, some local times don't exist (in New York on 2026-03-08, 02:30 never happens).
- Folds: when clocks fall back, some local times happen twice (01:30 on 2026-11-01 in New
York). The
foldattribute (0 = first occurrence, 1 = second) disambiguates. - Arithmetic is wall-clock arithmetic: adding
timedelta(days=1)to an aware datetime in aZoneInfozone keeps the same wall time and zone, which may be 23 or 25 real hours later. Subtracting two datetimes in the same zone object also ignores DST. For elapsed real time, convert to UTC first.
Regular expressions in depth
A regular expression is a small pattern language for matching text. Python's re module
compiles a pattern to a state machine and runs it with backtracking. Always write patterns as raw strings
(r"\d+") so backslashes reach re unchanged.
| Syntax | Meaning |
|---|---|
. \d \w \s (and \D \W \S) | any char (not newline unless DOTALL), digit, word char, whitespace (and negations) |
[a-z0-9_-], [^"] | character class, negated class |
* + ? {2,4} | greedy quantifiers; add ? for lazy: *?, +? |
^ $ \b \A \Z | anchors: line start/end (with MULTILINE), word boundary, string start/end |
(...) (?:...) (?P<name>...) | capturing, non-capturing, named group |
\1, (?P=name) | backreference to an earlier group |
(?=...) (?!...) (?<=...) (?<!...) | lookahead, negative lookahead, lookbehind (fixed width), negative lookbehind |
a|b | alternation (tried left to right) |
Functions: re.match (anchored at the start), re.fullmatch (whole string),
re.search (anywhere), re.findall (list of strings or group tuples),
re.finditer (match objects, lazily), re.sub (replace; the replacement can be a
function), re.split. Flags: re.IGNORECASE, re.MULTILINE,
re.DOTALL, re.VERBOSE, re.ASCII. re.compile builds a
reusable pattern object; the module also caches recently used patterns, but compiling makes intent and reuse
explicit.
Readable patterns with re.VERBOSE
With re.VERBOSE (re.X), whitespace in the pattern is ignored and # starts a
comment, so a complex pattern can be laid out and documented like code. (Escape spaces you mean literally as
\ or use [ ].)
argparse: command-line interfaces
argparse turns sys.argv into a validated namespace and generates --help
and error messages for free. The essentials: positional arguments, options with type,
default, choices, nargs, and action
("store_true", "count", "append"); mutually exclusive groups; and
subcommands with add_subparsers, which is how tools like git and pixi are
structured.
sqlite3: a real SQL database in the standard library
SQLite is an embedded SQL database: a single file (or ":memory:"), no server, transactional, and
astonishingly robust. It's ideal for local tools, caches, tests, and prototypes (M44
goes deeper). Three habits matter most:
- Always use placeholders (
?or:name), never f-strings, to pass values. Formatting user input into SQL is how SQL injection happens. - Use the connection as a context manager for transactions:
with conn:commits on success and rolls back on an exception. (It does not close the connection; usecontextlib.closingor callclose().) - Set
row_factory = sqlite3.Rowto access columns by name.
IDs, randomness, and hashing
uuid: uuid4 vs 3.14's uuid7
A UUID is a 128-bit identifier you can generate anywhere without coordination. The two you should know:
uuid4(): 122 random bits. Unpredictable and collision-safe, but random order: as database primary keys they scatter inserts across the index, which hurts B-tree locality.uuid7()3.14: a 48-bit Unix timestamp in milliseconds followed by random bits (RFC 9562). IDs generated later sort later, so they index like auto-increment integers while remaining globally unique. They do reveal creation time, so don't use them where that's sensitive.
Avoid uuid1() (embeds your MAC address). uuid5(namespace, name) gives a deterministic ID
for a name. 3.14 also adds uuid6, uuid8, and the uuid.NIL/uuid.MAX
constants.
secrets vs random
random uses the Mersenne Twister: fast, reproducible with a seed, and predictable
(observe 624 outputs and you can predict the rest). Perfect for simulations, sampling, shuffling test data, and
games. For anything an attacker must not guess (passwords, session tokens, reset links, API keys), use
secrets, which draws from the operating system's cryptographically secure generator.
hashlib and hmac
| Goal | Tool | Not |
|---|---|---|
| Fingerprint a file or detect corruption | hashlib.sha256, hashlib.file_digest(f, "sha256"), blake2b | md5/sha1 for anything security-related (broken) |
| Verify a message came from someone holding a shared key | hmac.new(key, msg, "sha256") + hmac.compare_digest | sha256(key + msg) (length-extension attacks) |
| Store passwords | A slow, salted KDF: hashlib.scrypt, hashlib.pbkdf2_hmac (or argon2 via a library) | Any fast hash, even SHA-256 |
Files: shutil, tempfile, and glob
pathlib (M09) handles paths; these modules handle the rest:
shutil:copy2(with metadata),copytree,move,rmtree(careful!),make_archive/unpack_archive(3.14 addszstdtar),which(find an executable on PATH),disk_usage. 3.14 also addsPath.copy()andPath.move().tempfile:TemporaryDirectory()andNamedTemporaryFile()create secure, uniquely named temporary locations and delete them when thewithblock exits. Never build temp paths by hand (/tmp/myapp.txt): it races with other processes and is a security hole.glob.glob("**/*.py", recursive=True)orPath.rglob("*.py"): find files by pattern.
enum, textwrap, and string
Enums (M13) give names to a fixed set of values. Recap of the
kinds: Enum (opaque members), IntEnum and StrEnum (members are
ints/strs, handy for JSON and CLI choices), Flag (combinable bits with |),
auto() for values, and @unique to forbid aliases. Iterating an enum class, and
Color["RED"], work thanks to its metaclass (M36).
textwrap formats prose: dedent (strip common indentation from
triple-quoted strings), fill/wrap, shorten (truncate at a word boundary
with a placeholder), and indent. string offers constants
(ascii_letters, digits, punctuation) and string.Template
($name substitution, safe for user-supplied templates because it can't evaluate expressions).
logging, and why Part 6 uses structlog
Beginner code uses print(); production code uses logging, because logs need levels
(debug noise off in production), timestamps, the source module, routing (console, file, log aggregator), and
configuration without code changes. The standard logging module provides all of that: loggers named
by module (logging.getLogger(__name__)), levels, handlers, and formatters.
The weakness of classic logging is that messages are text: to answer "how many 500s per path in the last
hour?", a log aggregator must parse human sentences. Structured logging emits events as key-value data
(usually JSON lines): {"event": "request", "path": "/api", "status": 500, "ms": 31}.
structlog makes that ergonomic and is the course's standard from Part 6
on (M42):
Recap: t-strings and string.templatelib
Practice
Write top_words(text, n, stopwords=()) returning the n most common words as
(word, count) pairs. Words are runs of letters and apostrophes (use re.findall with
r"[a-z']+" on the lowercased text), stopwords are excluded, and ties are broken
alphabetically.
Hint
Counter.most_common breaks ties by first appearance,
not alphabetically. Sort the items yourself with key=lambda kv: (-kv[1], kv[0]).
Show solution
import re
from collections import Counter
def top_words(text, n, stopwords=()):
stop = set(stopwords)
counts = Counter(w for w in re.findall(r"[a-z']+", text.lower()) if w not in stop)
return sorted(counts.items(), key=lambda kv: (-kv[1], kv[0]))[:n]
Write two lazy helpers. windows(iterable, n) yields overlapping tuples of length
n (use a deque(maxlen=n); it must work on infinite iterators). sessions(times,
gap) takes sorted timestamps (numbers) and yields lists of timestamps where consecutive items are at
most gap apart (a new session starts after a longer pause).
Hint
For windows, append each item to the deque and yield
tuple(window) once len(window) == n. For sessions, keep a current list
and the previous timestamp.
Show solution
from collections import deque
from itertools import count, islice
def windows(iterable, n):
window = deque(maxlen=n)
for item in iterable:
window.append(item)
if len(window) == n:
yield tuple(window)
def sessions(times, gap):
current = []
for t in times:
if current and t - current[-1] > gap:
yield current
current = []
current.append(t)
if current:
yield current
Write parse_line(line) for lines like
2026-07-01T09:15:00-04:00 WARN [auth] login failed user=bob ip=10.0.0.7. Return a dict with
keys ts, level, component, message (the free text before the
key=value pairs, stripped), and fields (a dict of the trailing key=value pairs), or
None if the line doesn't match. Levels are DEBUG, INFO,
WARN, or ERROR. Use a compiled pattern with named groups (re.VERBOSE
recommended).
Hint
Capture the whole tail after [component] as one group,
then split it: re.findall(r"(\w+)=(\S+)", tail) for fields, and
re.sub(r"\s*\w+=\S+", "", tail).strip() for the message. A timestamp pattern:
\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:Z|[+-]\d\d:\d\d).
Show solution
import re
LINE = re.compile(r"""
^(?P<ts>\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:Z|[+-]\d\d:\d\d))
\s+(?P<level>DEBUG|INFO|WARN|ERROR)
\s+\[(?P<component>[\w.-]+)\]
\s*(?P<tail>.*)$
""", re.VERBOSE)
FIELD = re.compile(r"(\w+)=(\S+)")
def parse_line(line):
m = LINE.match(line)
if m is None:
return None
tail = m["tail"]
return {
"ts": m["ts"],
"level": m["level"],
"component": m["component"],
"message": FIELD.sub("", tail).strip(),
"fields": dict(FIELD.findall(tail)),
}
Write to_zone(iso_string, zone) that parses an ISO 8601 timestamp with an offset and
returns an aware datetime in the IANA zone zone (raise ValueError for
naive input). Then write real_hours(start, end) that returns the actual elapsed hours between two
aware datetimes as a float, correct across DST changes even when both are in the same zone.
Hint
datetime.fromisoformat parses offsets; check
dt.tzinfo is None. For real elapsed time, convert both to UTC before subtracting, then divide by
timedelta(hours=1).
Show solution
try:
import tzdata
except ImportError:
pass
from datetime import UTC, datetime, timedelta
from zoneinfo import ZoneInfo
def to_zone(iso_string, zone):
dt = datetime.fromisoformat(iso_string)
if dt.tzinfo is None:
raise ValueError("timestamp must include an offset")
return dt.astimezone(ZoneInfo(zone))
def real_hours(start, end):
return (end.astimezone(UTC) - start.astimezone(UTC)) / timedelta(hours=1)
Write top_customers(orders, k, min_amount=0). orders is a list of
(customer, amount) tuples. Load them into an in-memory SQLite table with
executemany, then use one parameterized query to return the k
customers with the highest total among orders with amount >= min_amount, as a list of
(customer, total) sorted by total descending, then name. Close the connection before returning.
Hint
SELECT customer, SUM(amount) FROM orders WHERE amount >= ?
GROUP BY customer ORDER BY SUM(amount) DESC, customer LIMIT ? with parameters
(min_amount, k).
Show solution
import sqlite3
from contextlib import closing
def top_customers(orders, k, min_amount=0):
with closing(sqlite3.connect(":memory:")) as conn:
conn.execute("CREATE TABLE orders (customer TEXT, amount REAL)")
with conn:
conn.executemany("INSERT INTO orders VALUES (?, ?)", orders)
rows = conn.execute(
"SELECT customer, SUM(amount) AS total FROM orders WHERE amount >= ? "
"GROUP BY customer ORDER BY total DESC, customer LIMIT ?",
(min_amount, k),
).fetchall()
return [(name, total) for name, total in rows]
Mini-project: a log-parsing CLI
Quiz
Recap
deque,Counter,defaultdict,ChainMap, andnamedtuplereplace common hand-written patterns.itertoolsis lazy:pairwise,batched,accumulate,chain, andgroupby(consecutive keys) cover most needs.- Use aware datetimes, store UTC, display local; ZoneInfo arithmetic is wall-clock, so convert to UTC for elapsed time; mind gaps and folds.
- Compile regexes, use named groups and
re.VERBOSE, use lookarounds for conditions, and beware catastrophic backtracking. argparsesubcommands withmain(argv) -> int; 3.14 adds color andsuggest_on_error.sqlite3: placeholders always,with conn:for transactions,sqlite3.Rowfor names.uuid7for sortable IDs,secretsfor tokens, HMAC for message authentication, slow KDFs for passwords.tempfile,shutil,glob,enum,textwrap,logging, and t-strings round out the toolkit.