R03 · Week 3 Recall & Drill
Week 3 revision: exception discipline and pdb, import resolution and packaging, pytest fixtures and the TDD loop, type hints that no runtime enforces, and Big-O as a growth rate.
🎯 Rebuild Week 3 from a blank page: catch only what you can handle, imports resolve once per process, coverage is a negative signal, annotations are metadata, and Big-O is a limit not a benchmark.
Weekly revision · Week 3 · Covers 5 sessions from Mon–Fri.
Sessions covered
- S011 — Errors, Exceptions & Debugging with pdb
- S012 — Modules, Packages, Virtualenvs, pip & uv
- S013 — Testing with pytest — TDD Workflow
- S014 — Type Hints, mypy, dataclasses & pydantic
- S015 — Big-O Notation — Reasoning About Scale
- Read any traceback bottom-to-top, locate the failing frame, and say why a bare except turns a loud failure into a silent wrong answer.
- Drive pdb with n, s, c, l, p, w, u, d, b, q — and drop into a debugger without editing the source file.
- Trace how import resolves through sys.path, and explain why a module executes once per process and is then cached in sys.modules.
- Turn a folder of .py files into an installable package with pyproject.toml and an editable install.
- Write pytest tests using assert, parametrize, fixtures, and monkeypatch — and explain why coverage is a good negative signal and a poor positive one.
- State the Big-O of a loop on sight, and explain when a theoretically worse algorithm wins in practice.
90-min structure
| Block | Minutes | What you do |
|---|---|---|
| Warm-up recall | 5 | Five sessions, one sentence each, no tabs. |
| Blank-page reconstruction | 30 | The per-session prompts below. |
| Hands-on drill | 30 | Package + test + type + measure, in one repo. |
| Quiz + misconception | 15 | Answer before revealing. |
| Gap analysis + preview | 10 | Write the gaps. Skim next week. |
Blank-page reconstruction · 30 min
S011 · Errors, Exceptions & Debugging
- Sketch the parts of the exception hierarchy you actually touch, and say why
except Exceptionandexcept BaseExceptionare different mistakes. - Write a
try/except/else/finallyblock and state, in one sentence each, what belongs inelseand what belongs infinally. - List the pdb keystrokes for step-over, step-into, continue, list source, print, where, up, down, breakpoint, quit.
Gotcha you probably forgot:
raise NewError()inside anexceptblock discards the original cause from the reader's point of view — you get the new error with a confusing implicit context.raise NewError() from originalexplicitly chains it, so the traceback says "the above exception was the direct cause of the following exception". Also:log.exception(msg)inside an except block records the traceback;log.error(msg)throws it away.
S012 · Modules & Packaging
- Define module, package, and distribution in one sentence each.
- Write the order Python searches when you type
import foo, and name the two most common ways that order surprises people. - Name the two blocks every
pyproject.tomlneeds and what each one does.
Gotcha you probably forgot: a module is executed once per process, on first import, and the resulting module object is cached in
sys.modules. Every later import is a dictionary lookup returning the same object. That is why module-level state behaves as a process-wide singleton, and why re-importing does not pick up your edits — you need a fresh process.
S013 · Testing with pytest
- Give a concrete one-line example each of a unit test, an integration test, and an end-to-end test for the same feature.
- Explain what a fixture buys you over a plain helper function, and describe the fixture lifecycle.
- Write the red → green → refactor loop as three sentences describing what you are allowed to do in each phase.
Gotcha you probably forgot:
pytest.raises(ValueError)passes for the wrongValueError. If your function raises a validation error for a completely different reason, the test still goes green. Always constrain it:pytest.raises(ValueError, match="must start with http"). The same discipline applies to mocks — asserting a call happened is weaker than asserting it happened with the right arguments.
S014 · Type Hints, mypy, dataclasses & pydantic
- Read this aloud and translate it into English:
def f(xs: list[User]) -> dict[str, int] | None:. - Build the decision table:
dict,TypedDict,dataclass,NamedTuple,pydantic.BaseModel— one sentence on when each is right. - Say what
@dataclass(frozen=True, slots=True)buys you, on both correctness and memory.
Gotcha you probably forgot: annotations are not enforced at runtime. Python stores them in
__annotations__and never checks them, sodef f(x: int)happily accepts a string. Enforcement requires an external checker such as mypy, or a library like pydantic that explicitly validates at the boundary. Type hints inside your own code are documentation plus static analysis; pydantic at the edge of your system is the actual validation.
S015 · Big-O Notation
- Sketch the six curves — constant, log n, n, n log n, n squared, 2 to the n — on the same axes, and mark roughly where each overtakes the others.
- Write the complexity table for list, dict, and set across index, append, membership, and delete.
- Define worst-case, average-case, and amortised, with one concrete example each.
Gotcha you probably forgot:
list.appendis amortised constant time, not constant time. Occasionally the underlying array is full and Python allocates a bigger one and copies everything, which is a linear operation. Averaged over many appends the cost per append is still constant, and that averaging is exactly what "amortised" means.
Hands-on drill · 30 min
Task: take a loose script and turn it into an installed, typed, tested package — then measure the Big-O claim instead of asserting it.
Step 1 — package skeleton (7 min)
mkdir -p ~/projects/w3-drill/src/wordstats && cd ~/projects/w3-drill
uv venv .venv --python 3.12 && source .venv/bin/activate
cat > pyproject.toml <<'TOML'
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "wordstats"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = []
[project.optional-dependencies]
dev = ["pytest", "mypy"]
TOML
touch src/wordstats/__init__.pyStep 2 — the module, typed (8 min)
# src/wordstats/core.py
from collections import Counter
from dataclasses import dataclass
class WordStatsError(Exception):
"""Base for everything this package raises."""
class EmptyTextError(WordStatsError):
"""Raised when the input has no words at all."""
@dataclass(frozen=True, slots=True)
class Stats:
total: int
distinct: int
top: list[tuple[str, int]]
def analyse(text: str, top_n: int = 3) -> Stats:
if top_n < 1:
raise ValueError("top_n must be >= 1")
words = [w.strip(".,!?;:").lower() for w in text.split()]
words = [w for w in words if w]
if not words:
raise EmptyTextError("no words found in input")
counts = Counter(words)
return Stats(total=len(words), distinct=len(counts), top=counts.most_common(top_n))uv pip install -e ".[dev]"
python -c "from wordstats.core import analyse; print(analyse('the cat the hat the end'))"Expected outcome: uv pip install -e . makes the package importable from anywhere in the venv without setting PYTHONPATH, and the print shows total 6, distinct 4, and the at the top with a count of 3.
Step 3 — tests, with the constrained assertion (8 min)
# tests/test_core.py
import pytest
from wordstats.core import EmptyTextError, analyse
def test_counts_and_distinct():
s = analyse("the cat the hat the end")
assert s.total == 6
assert s.distinct == 4
assert s.top[0] == ("the", 3)
@pytest.mark.parametrize("text", ["", " ", "\n\t "])
def test_empty_input_raises(text):
with pytest.raises(EmptyTextError, match="no words found"):
analyse(text)
def test_bad_top_n_raises_value_error():
with pytest.raises(ValueError, match="top_n must be"):
analyse("hello world", top_n=0)
def test_frozen_dataclass_is_immutable():
s = analyse("a b c")
with pytest.raises(AttributeError):
s.total = 99pytest -q
mypy src/wordstatsExpected outcome: four tests pass (the parametrized one counts as three cases, so pytest reports six), and mypy reports no issues. If mypy complains about most_common, that is a genuine signal about your annotation, not noise — fix the type rather than silencing it.
Step 4 — measure the Big-O claim (7 min)
Do not take "membership in a list is linear" on faith:
# bench.py
import time
for n in (1_000, 10_000, 100_000):
xs = list(range(n))
ss = set(xs)
target = n - 1 # worst case for the list: last element
t0 = time.perf_counter()
for _ in range(1000):
target in xs
list_time = time.perf_counter() - t0
t0 = time.perf_counter()
for _ in range(1000):
target in ss
set_time = time.perf_counter() - t0
print(f"n={n:>7} list={list_time:.4f}s set={set_time:.4f}s")Expected outcome: the list column grows roughly in proportion to n — each tenfold increase in n produces close to a tenfold increase in time — while the set column stays essentially flat across all three sizes. That flat column is what constant-time membership looks like when you measure it instead of quoting it. Absolute numbers depend entirely on your machine; the ratio between rows is the thing to read.
"We are at 90% line coverage, so the code is verified. A test that runs the code without crashing proves the code works."
Coverage measures which lines executed, not which behaviours were checked — a suite with zero assertions can reach 100%. Treat coverage as a reliable negative signal: uncovered lines are definitely untested, and that is genuinely useful. As a positive signal it is close to worthless. The question that actually matters is "if I broke this behaviour, would a test go red?" — and the honest way to answer it is to break the behaviour on purpose and watch.
Gap analysis + next week preview · 10 min
- Did any test in Step 3 pass for the wrong reason? Delete one assertion, rerun, and confirm the suite goes red. A suite that stays green when you break the code is decoration.
- Did mypy find something you did not expect? Write the annotation down — the ones that surprise you are the ones that would have become bugs.
- In the benchmark, was the ratio between rows what you predicted before running it? If not, your intuition about the growth rate needs another pass at S015.
Next week (S016–S020) turns from code to the mathematics underneath it: discrete math with sets, logic, combinatorics and graphs; linear algebra with vectors, dot products and geometry; then matrices, transforms and eigenvalues; calculus with derivatives and the chain rule; and finally gradients and gradient descent implemented from scratch. The Big-O work you just did is the bridge — it is the first place the plan asks you to reason about a function's behaviour rather than its output.
Part of the 6-month evergreen learning plan.