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.

⏱ ~2.5 hoursPrerequisites: M06 Collections, M18 Functional Programming

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.

TypeReplacesKey operations
dequeA list used as a queue (pop(0) is O(n))O(1) append/appendleft/pop/popleft; maxlen for a ring buffer; rotate
Counterd[k] = d.get(k, 0) + 1most_common(n), update, arithmetic + - & |, total()
defaultdictd.setdefault(k, []).append(v)Calls a factory for missing keys (list, set, int)
ChainMapMerging dicts for lookupSearches several mappings in order without copying: CLI args → env → defaults
namedtupleBare tuples with magic indexesNamed 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).

FamilyTools
Infinitecount, cycle, repeat
Slicing and filteringislice, takewhile, dropwhile, filterfalse, compress
Combiningchain, chain.from_iterable, zip_longest, pairwise (3.10), batched (3.12; strict= 3.13)
Grouping and reducinggroupby (consecutive keys!), accumulate, starmap, tee
Combinatoricsproduct, 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:

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.

SyntaxMeaning
. \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 \Zanchors: 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|balternation (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:

  1. Always use placeholders (? or :name), never f-strings, to pass values. Formatting user input into SQL is how SQL injection happens.
  2. 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; use contextlib.closing or call close().)
  3. Set row_factory = sqlite3.Row to 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:

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

GoalToolNot
Fingerprint a file or detect corruptionhashlib.sha256, hashlib.file_digest(f, "sha256"), blake2bmd5/sha1 for anything security-related (broken)
Verify a message came from someone holding a shared keyhmac.new(key, msg, "sha256") + hmac.compare_digestsha256(key + msg) (length-extension attacks)
Store passwordsA 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:

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

Mini-projectlogtool: regex, time zones, and a real CLI

Build logtool, a command-line tool that reads application logs from servers in several time zones (lines like Exercise 3's, with mixed offsets), normalizes every timestamp to UTC, and answers operational questions in the user's own time zone.

pixi init logtool
cd logtool
pixi add python=3.14 pytest
pixi run python -m logtool stats sample.log --tz America/New_York --top 5
pixi run python -m logtool grep sample.log --level ERROR --since 2026-07-01T08:00 --tz Europe/Oslo --format json
pixi run python -m logtool hist sample.log --bucket 15m --tz Asia/Kolkata
pixi run python -m pytest -q

Specification

  • A compiled, re.VERBOSE pattern with named groups parses each line; unparseable lines are counted and reported with their line numbers, never crash the tool.
  • Timestamps with any offset (or Z) become aware UTC datetimes; output is rendered in --tz (an IANA name validated with zoneinfo.available_timezones() or by catching ZoneInfoNotFoundError). --since/--until given as local wall times are interpreted in --tz.
  • stats: counts per level and component (Counter), top messages, busiest hour (local), and error rate.
  • grep: filters by level, component, time range, and a regex on the message; text or JSON Lines output.
  • hist: a text histogram per time bucket (itertools.groupby on sorted buckets).
  • main(argv=None) -> int with argparse subcommands, suggest_on_error=True, and exit codes 0 (ok), 1 (no matches), 2 (usage or input errors).

Acceptance criteria

  • Lines with -04:00, +05:30, and Z offsets sort correctly in one timeline
  • A --since during a DST transition in --tz is handled (test the fold and the gap)
  • Unparseable lines are reported with line numbers and don't change the exit code unless all lines fail
  • stats output in two different zones shows the same counts but different busiest-hour labels
  • At least 15 pytest tests, using tmp_path for sample files, pass with pixi run python -m pytest

Stretch goals

  • Stream gzip- or zstd-compressed logs transparently (gzip.open, 3.14's compression.zstd).
  • Store parsed events in SQLite and answer stats with SQL.
  • Emit the tool's own diagnostics with logging, then port them to structlog after M42.
  • Profile it on a million lines (M39) and make it fast.

Quiz

Recap

Going further