Dinesh/ Blog
learning

R03 · Week 3 Recall & Drill

Week 3 revision: exception discipline and pdb, import resolution and packaging, pytest fixtures and the TDD loop, type hints without automatic enforcement by ordinary Python execution, and Big-O as a growth rate.

20 min readChecking device speech…
Lesson preparation & details

Level: intermediate

By the end, you should be able to

  • Locate the failing frame and exception in a traceback, and distinguish legitimate boundary handling from a handler that silently suppresses failure.
  • 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 how normal imports reuse sys.modules and why reload or distinct interpreter/cache state changes that behavior.
  • 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.
  • Derive loop complexity from iteration counts, operation costs and input assumptions; explain why asymptotic ranking can differ from small-workload timings.

Bring with you

Sources checked: . A source check does not change the publication or update date.

Editorial review: · What review means

In this article · 23 sections

Assessment scope

Attempt the recall prompts before reading the worked drill and answer explanations. The worked examples below restore the combined teaching task, with explicit contracts and checks; a local model is distinguished from a reproduced external service. The minute allocations are adjustable practice suggestions, not evidence of learning speed.

Sessions covered

By the end of this revision you can

  1. Locate the failing frame and exception in a traceback, and distinguish legitimate boundary handling from a handler that silently suppresses failure.

  2. Drive pdb with n, s, c, l, p, w, u, d, b, q — and drop into a debugger without editing the source file.

  3. Trace how import resolves through sys.path, and explain how normal imports reuse sys.modules and why reload or distinct interpreter/cache state changes that behavior.

  4. Turn a folder of .py files into an installable package with pyproject.toml and an editable install.

  5. Write pytest tests using assert, parametrize, fixtures, and monkeypatch — and explain why coverage is a good negative signal and a poor positive one.

  6. Derive loop complexity from iteration counts, operation costs and input assumptions; explain why asymptotic ranking can differ from small-workload timings.


90-min structure

Treat this as an adjustable practice budget: 5 minutes to recall the five subjects, 30 for the blank-page questions, 30 for the worked drill, 15 to compare answers and test a counterexample, and 10 to record a gap and plan an unseen retest. The solutions and transfer exercises can take longer; the goal is correct reasoning, not racing a timer.

Blank-page reconstruction · 30 min

S011 · Errors, Exceptions & Debugging

  1. Sketch the parts of the exception hierarchy you actually touch, and explain what each catches and when a broad boundary handler is justified.
  2. Write a try / except / else / finally block and state, in one sentence each, what belongs in else and what belongs in finally.
  3. List the pdb keystrokes for step-over, step-into, continue, list source, print, where, up, down, breakpoint, quit.

S012 · Modules & Packaging

  1. Define module, package, and distribution in one sentence each.
  2. Write the order Python searches when you type import foo, and name the two most common ways that order surprises people.
  3. Explain the build-system and project tables in this package's pyproject.toml without asserting every use of the file requires both.

S013 · Testing with pytest

  1. Give a concrete one-line example each of a unit test, an integration test, and an end-to-end test for the same feature.
  2. Explain what a fixture buys you over a plain helper function, and describe the fixture lifecycle.
  3. Write the red → green → refactor loop as three sentences describing what you are allowed to do in each phase.

S014 · Type Hints, mypy, dataclasses & pydantic

  1. Read this aloud and translate it into English: def f(xs: list[User]) -> dict[str, int] | None:.
  2. Build the decision table: dict, TypedDict, dataclass, NamedTuple, pydantic.BaseModel — one sentence on when each is right.
  3. Say what @dataclass(frozen=True, slots=True) buys you, on both correctness and memory.

S015 · Big-O Notation

  1. 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.
  2. Write the complexity table for list, dict, and set across index, append, membership, and delete.
  3. Define worst-case, average-case, and amortised, with one concrete example each.

Hands-on drill · 30 min

Task: take a loose script and turn it into an installed, typed, tested package — then compare bounded measurements with an operation-count argument. The package installation, debugger and mypy commands below are optional instructions for a disposable environment, not executed-install evidence.

Step 1 — package skeleton (7 min)

mkdir -p src/wordstats tests
python3 -m venv .venv
# In a new disposable exercise directory; invoke .venv/bin/python explicitly.
 
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__.py

Step 2 — the module, typed (8 min)

# src/wordstats/core.py
from collections import Counter
from dataclasses import dataclass
 
 
class WordStatsError(Exception):
    """Base for this package's domain-specific errors."""
 
 
class EmptyTextError(WordStatsError):
    """Raised when the input has no words at all."""
 
 
@dataclass(frozen=True, slots=True)
class Stats:
    total: int
    distinct: int
    top: tuple[tuple[str, int], ...]
 
 
def analyse(text: str, top_n: int = 3) -> Stats:
    if not isinstance(text, str):
        raise TypeError("text must be str")
    if type(top_n) is not int or 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=tuple(counts.most_common(top_n)))
.venv/bin/python -m pip install -e ".[dev]"
.venv/bin/python -c "from wordstats.core import analyse; print(analyse('the cat the hat the end'))"

Expected outcome: an editable install makes the package importable from anywhere in the venv in a fresh interpreter without setting PYTHONPATH (subject to import shadowing), 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 = 99
.venv/bin/python -m pytest -q
.venv/bin/python -m mypy src/wordstats

The four test functions define six cases because the empty-input test has three parameters. Run and inspect both tools rather than assuming their result; versions and configuration affect diagnostics. frozen=True prevents ordinary field rebinding, not deep mutation; the repaired top uses tuples of immutable values. Deliberately mutate the implementation (for example, stop lowercasing) and require a case-policy test to fail. Deleting an assertion does not normally make a suite fail.

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 (100, 1_000, 5_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(100):
        target in xs
    list_time = time.perf_counter() - t0
 
    t0 = time.perf_counter()
    for _ in range(100):
        target in ss
    set_time = time.perf_counter() - t0
 
    print(f"n={n:>7}  list={list_time:.4f}s  set={set_time:.4f}s")

A successful last-position search compares up to N elements; a missing item also takes N comparisons. Repeating the query Q times gives O(QN). Building the set costs expected O(N) time and O(N) space, after which Q ordinary-hash lookups cost expected O(Q). For one lookup, construction can dominate. Small timings include timer noise, hashing/equality costs and cache effects; ratios are observations, not proofs of asymptotic bounds.

Exceptions, debugger and import answers

For int("bad"), the final traceback line identifies ValueError; the preceding frames locate the call chain. Catch a specific expected input error and either recover or raise WordStatsError("invalid input") from exc. Implicit exception context is already retained when raising inside an except; from exc makes causation explicit rather than recovering discarded information. The relevant hierarchy is BaseException above Exception (which includes ValueError, TypeError and OSError), with KeyboardInterrupt and SystemExit directly under BaseException rather than Exception. Bare except also catches KeyboardInterrupt and SystemExit; broad handlers are reasonable at a process boundary if they log and propagate/exit rather than manufacture success. logging.exception includes the current traceback in an exception handler; logging.error can also include it when exc_info=True, so the distinction is a default, not an absolute inability. else holds work after a successful try; finally releases resources regardless of ordinary exit or exception. Use a context manager for files.

For a saved failing script, python -m pdb script.py starts a debugger without source changes. n steps over, s steps into, c continues, l lists source, p expression prints, w shows the stack, u/d select callers/callees, b file:line sets a breakpoint, and q quits. Avoid evaluating expressions with side effects while inspecting state. The interactive debugger was not driven by these automated checks.

Import first consults sys.modules and uses import finders (including builtins/frozen modules and path-based finders); it is not simply a filesystem scan. A local json.py can shadow the library. The usual cached import does not rerun a module, but explicit reload, deletion from the cache and separate interpreters are exceptions. A module is an importable namespace, a package can contain submodules, and an installed distribution can supply several import packages. In this build, [build-system] declares the backend and [project] declares metadata; not every use of pyproject.toml needs both tables. Editable installs expose source edits to fresh imports but do not refresh existing objects or reliably update generated metadata/extensions without further work.

Tests, validation and CLI extension

A unit test checks analyse directly; an integration test invokes the CLI and package together; an end-to-end test installs the distribution in an isolated environment and calls its public entry point. A fixture supplies setup/teardown and controlled scope; parametrization exercises separate cases; monkeypatch temporarily replaces the attribute looked up by the tested code and restores it afterward. A clock dependency can be passed explicitly to avoid sleeping. Red: observe a meaningful failing test. Green: implement the behavior. Refactor: improve structure while keeping the suite green. Coverage shows executed lines under the recorded suite, not verified behaviors.

A plain dict is flexible; TypedDict describes a dict to a static checker; dataclass provides named fields and generated methods; NamedTuple is a tuple-compatible record; a validation library such as Pydantic validates according to its configured coercion/strictness rules. Ordinary annotations do not check arguments at runtime, and their evaluation/storage is version-sensitive. Slots generally remove an instance dictionary for these instances but are not a portable byte-saving guarantee. frozen is not recursive freezing.

For the current worksheet's CLI extension, place the following in src/wordstats/__main__.py. Parsing fails with a usage error for a non-integer --top; domain errors from analyse are turned into parser errors. Reading stdin is deferred until after arguments parse.

# src/wordstats/__main__.py (separate module, not concatenated with core.py)
import argparse
import sys
from .core import analyse, WordStatsError
parser = argparse.ArgumentParser()
parser.add_argument("--top", type=int, default=3)
args = parser.parse_args()
try:
    print(analyse(sys.stdin.read(), args.top))
except (WordStatsError, ValueError, TypeError) as exc:
    parser.error(str(exc))

Test mixed case/punctuation with analyse("Cat, cat! DOG"): total 3, distinct 2, first pair ("cat",2). Test all-punctuation input as empty. The function's contract strips the listed punctuation only; it does not claim language-independent tokenization. Tie order follows first occurrence through Counter, so test that policy rather than assuming alphabetical ordering.

Fixtures, monkeypatch and a discriminating regression

Add these tests to tests/test_core.py after the existing tests. The default fixture scope is one test; the yield separates setup from teardown. Resources acquired before yield need cleanup protection if setup itself can fail. monkeypatch restores its changes after the requesting test, and the patch targets the attribute the function actually reads.

# Additional tests/test_core.py content; uses pytest and analyse imported above.
import time
 
@pytest.fixture
def sample_text():
    lines = ['Cat, cat! DOG']
    yield lines
    lines.clear()
 
def test_case_policy(sample_text):
    assert analyse(sample_text[0]).top == (('cat', 2), ('dog', 1))
 
def timestamped_summary(text):
    return time.time(), analyse(text)
 
def test_clock_is_controlled(monkeypatch):
    monkeypatch.setattr(time, 'time', lambda: 123.0)
    stamp, stats = timestamped_summary('one one two')
    assert stamp == 123.0 and stats.total == 3
 
@pytest.mark.parametrize('text', ['!!!', '.,;:'])
def test_punctuation_only_is_empty(text):
    with pytest.raises(EmptyTextError, match='no words found'):
        analyse(text)

The original six pytest cases plus these four cases give ten cases. If you remove .lower() from the implementation, test_case_policy now fails because Cat and cat no longer share a count. This is a genuine mutation check, unlike deleting an assertion. The clock test checks a seam without sleeping; it does not prove an end-to-end installed CLI works.

A type such as list[User] -> dict[str,int] | None says the input is a list of User objects and the return is either the mapping or None. A static checker can reject contradictory uses without executing the function, but runtime validation still requires explicit checks. In Stats, slots remove the ordinary instance dictionary and frozen prevents normal field assignment; neither guarantees global immutability for arbitrary nested fields.

Recall completion: error blocks and operation costs

The syntax question asks for a complete block, not only names of clauses. In this example a bad conversion is an expected input failure; else runs only on success, and finally records cleanup on either path. Exceptions raised inside else are not caught by the preceding except.

def parse_count(raw, events):
    try:
        count = int(raw)
    except ValueError:
        events.append('invalid')
        return None
    else:
        events.append('valid')
        return count
    finally:
        events.append('cleanup')
events = []
assert parse_count('12', events) == 12 and events == ['valid', 'cleanup']
events = []
assert parse_count('bad', events) is None and events == ['invalid', 'cleanup']

For path-based imports, inspect sys.path in the interpreter actually running the program. Its ordinary initialization includes the script directory (or current directory in relevant interactive/-m modes), PYTHONPATH, and installation/site paths; isolated/safe-path modes and import hooks can alter this. A local module shadowing the library and invoking a different interpreter/venv are distinct failure modes.

The requested container table is about operations, not a single complexity label for each container:

Operationlistdictset
Index/key accessO(1) by integer indexexpected O(1) by keyno positional indexing
Append/addamortized O(1) appendamortized expected O(1) key assignmentamortized expected O(1) add
MembershipO(n)expected O(1) for keysexpected O(1)
DeleteO(n) by value/front; O(1) end popexpected O(1) by key; O(n) value searchexpected O(1) by value

Hash bounds assume bounded-cost hashing/equality and ordinary nonadversarial hash distribution; resizes are amortized across operations. An expensive key comparison changes these costs. The pytest.raises(..., match=...) examples constrain both error type and message: a bare raises(ValueError) could pass for an unrelated validation failure. Neither form proves the rest of the behavior, and message checks should target a stable contract rather than incidental wording.

Operation counts before timing

For positive integer n, one fixed operation is O(1); repeatedly halving n takes O(log n); one full scan O(n); n scans of logarithmic depth O(n log n); two independent full nested scans O(n²); enumerating all subsets visits 2ⁿ states. These are upper bounds under stated primitive-operation costs, not unique labels determined by syntax. For example, a loop whose inner pointer advances at most n times across the whole run is O(n), even though it is nested.

Worst-case asks for the maximum cost over size-n inputs (a missing list key takes n comparisons). Average-case requires an input distribution (uniformly located successful list search averages about n/2 comparisons). Amortized analysis bounds a sequence of operations without a probability distribution: occasional O(n) array resizing is paid across many O(1) appends, yielding O(1) amortized append. Big-O suppresses constants; there is no universal crossover n between two implementation families. A quadratic contiguous loop can beat a pointer-heavy n-log-n implementation on small inputs without contradicting either bound.

Debugging exercise: save int('bad') as a disposable script, invoke .venv/bin/python -m pdb script.py, set a breakpoint on that line with b 1, use l and p 'bad', then n to reproduce ValueError. In post-mortem mode w shows the failing frame and u/d select frames. s enters a called Python function whereas n steps over it. Quit with q. This provides a concrete command sequence; interactive user proficiency and package installation are not inferred from unit-test success.

Answer constraints and corrections

AAA separates fixture setup, operation and assertion. Coverage does not certify behavior. A type annotation does not validate calls. Timings may support an operation-count analysis but cannot prove Big-O. Editable installation does not refresh an already-cached module in a running interpreter.

Gap analysis + next week preview · 10 min

Score each original recall question separately: explain unaided; explain with a hint; or not yet. A fluent verbal answer is not a successful implementation. Choose one failed concept, write a counterexample that distinguishes the mistaken rule from the correct one, and schedule an unseen retest. Keep successes as well as failures in the log so the plan does not become a list of increasingly familiar problems.

For the drill, require an explicit input contract, a correctness argument or domain assumption, one ordinary case, one boundary case and an independent expected result. A chart, compile success or passing sample alone does not meet that rubric. Use the stated input contracts and named runtime when interpreting a result; finite tests and timing samples are not universal correctness or performance guarantees.

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.

Corrective source notes

The cited passages support these named claims. The algebra, counterexamples and executable fixtures supply the lesson-specific reasoning; finite checks are not universal benchmarks.

Pause / Recall / Apply

Can you explain it without the page?

Close the example. Reconstruct the core idea, then change one assumption. Mark complete when you’re ready; you can always undo it.

Stored in this browser only. No account, no sync. Clearing browser data removes your record.