0.1From scripts to programs 🟢 Browser-safe
You finished Foundations able to write ~100-line programs: variables, loops, lists and dicts, functions, simple classes, files with try/except. This course takes you from there to 200–400-line tested, CLI-driven programs built from the standard library plus pytest. Same language — bigger workshop.
| Foundations (you can) | Intermediate (you will) |
|---|---|
| One-file scripts, ~100 lines | Multi-file programs, 200–400 lines |
| Loops that build lists | Comprehensions, generators, lazy pipelines |
Classes with __init__ | Dataclasses, dunders, properties, custom exceptions |
print() debugging | logging, tracebacks, pytest, pdb |
| Run by pressing a button | Run from a terminal: venv, argparse, real files |
The loop stays — plus one new layer
Keep the Foundations habit verbatim: Read → Predict → Run → Tinker → Break → Fix → Build. Intermediate adds a layer between Predict and Run: Trace. Before running, ask "what is x at line N?" and work it out on paper. Solutions in this course show trace tables, not just answers.
# I think this prints ...) before pressing Run. Wrong predictions are the fastest lessons in programming; this course is engineered to provoke them.while True: with no break locks the page — that's the price of real Python in a tab. If it happens, reload: your code is auto-saved. Module 3 teaches lazy (possibly infinite) generators; every one of those boxes is guarded with islice or a counter.What this course is not
Scope guard, stated once so no lesson smuggles these in: no async mastery, no metaclasses or descriptors, no Django/Flask production, no pandas/numpy track, no ML, no PyPI publishing pipeline (local pip install -e . at most). Suggestions in that direction belong in extensions.
Where you're headed
| Module | Question it answers |
|---|---|
| 1 · Pythonic Data | How do I stop writing loops for everything? |
| 2 · Functions as Objects | What can I do once functions are values? |
| 3 · Lazy Python | How do I stream data bigger than memory? |
| 4 · OOP That Pays | Which object tricks earn their complexity? |
| 5 · Files, Data & Storage | How does data survive the program ending? |
| 6 · Tooling & Quality | How do professionals run Python? |
| 7 · Power Tools | How do decorators and context managers really work? |
| 8 · Capstones | Can I ship three portfolio programs? |
0.2Entry diagnostic — are you ready? 🟢 Browser-safe
Ten small tasks below, covering Foundations Modules 2–6. Rules: no hints, no peeking — do each from memory, then check against the solution. Count a task as passed only if your unaided version works.
- 7+/10: continue to Module 1.
- 5–6/10: skim the linked Foundations modules first (table below), then continue.
- Below 5: redo Foundations Modules 2–6 properly, then come back. Module 1 assumes all of this is fluent.
Exercise 0.1 — Ten tasks from memory
Fix or finish each TODO so the box prints the expected values in the comments. Every task runs as-is (wrongly, or with a crash) — a crash just means "not solved yet".
💡 Hints
- 1:
if score >= 60:…else:… (prose uses>=; in code just type it). - 2:
range(1, 11)stops before 11. - 3:
colors[0]andcolors[-1]. - 4:
counts["cherry"] = counts.get("cherry", 0) + 1. - 5:
def greet(name="friend"):— then callgreet()andgreet("Ada"). - 6:
d = json.loads(data). - 7: methods need
selfas the first parameter. - 8:
b = Basket(), thenb.add("apple"). - 9:
try:the conversion,except ValueError:print the fallback. - 10:
with open("note.txt", "w")…write(...), then again with"r"andread().
Show solution
If you scored under 7 — revisit map
| Tasks | Tests | Revisit in Foundations |
|---|---|---|
| 1–2 | decisions, loops | Module 2 · Control Flow |
| 3–4 | lists, dicts | Module 3 · Data Structures |
| 5–6 | functions, modules | Module 4 · Functions & Modules |
| 7–8 | classes, methods | Module 5 · Objects & Classes |
| 9–10 | errors, files | Module 6 · Files & Errors |
0.3Your two machines — browser and terminal 🔴 Terminal-required
This course runs on two machines: the browser tab you're in now, and the terminal on your own computer. Rule of thumb for the whole course: browser for concepts, terminal (or the desktop edition) for real files, CLIs, venv, and pytest. Every lesson wears one badge so you always know where you are:
| Badge | Meaning | You… |
|---|---|---|
| 🟢 Browser-safe | Runs identically in this tab (Pyodide) | press Run here |
| 🟡 Browser-adapted | Same concept, shimmed I/O (in-memory files) | run here, read the banner, redo on your PC with real files |
| 🔴 Terminal-required | Needs a real CLI: venv, multi-file, pytest | do it on your PC — the browser box is read-only |
Step 1 — check your Python
Open a terminal (Terminal on macOS/Linux/ChromeOS, PowerShell on Windows) and run:
python means Python 2 or nothing at all. Always type python3 — and if even that fails, install Python 3.9+ from python.org, then come back. The desktop edition of this course must be launched the same way: python3 py-intermediate.py.Step 2 — one project, one venv
A venv is a private Python playground per project: its own packages, no interference between projects. Make one where you'll keep course work:
| OS | Create | Activate |
|---|---|---|
| macOS / Linux / ChromeOS | python3 -m venv .venv | source .venv/bin/activate |
| Windows (PowerShell) | py -m venv .venv | .venv\Scripts\Activate.ps1 |
mkdir pywork && cd pywork— one folder for all course work.python3 -m venv .venv— create the playground (once per folder).- Activate it (table above) — your prompt gains a
(.venv)prefix. python -m pip install pytest— expected output ends withSuccessfully installed pytest-....
sudo pip install ... (or bare pip install outside a venv) sprays packages into your system Python, where the OS depends on exact versions. Inside an active venv, plain pip is safe — the (.venv) prefix is your proof.python -m pip ... and python -m pytest always use that python's packages — even if you forgot to activate. If a command ever "can't find" an installed package, re-run it as python -m ... first.Step 3 — how a grown-up program is laid out
From Module 6 on, starters look like this — a folder, not a file. The browser shows them flattened; your PC uses the real tree:
Why the odd if? It means "run this only when the file is executed directly, not when imported for tests". Module 6 makes it second nature.
One more machine worth knowing: the desktop edition (python3 py-intermediate.py) sits between the two — same lessons, but code runs in a real subprocess with real files that persist in ~/.pyintermediate/workspace/. Browser for concepts, desktop/terminal for reality.
0.4Tracebacks — read the last line first 🟢 Browser-safe
You met tracebacks in Foundations; here they become your primary debugging tool. Anatomy of one — schematic, with notes where a real one names files:
Read bottom-up: the last line names the error type and cause; the frames above are the call chain that got there, outermost first. Then fix, rerun, repeat.
NameError names the unknown variable, and modern Python even suggests the fix.
Show fix
Last line: NameError: name 'username' is not defined. Did you mean: 'user_name'? — the variable is user_name (with underscore), the printout says username. Fix one character class:
ValueError — but the bug (bad input) is two frames up. Read bottom-up to find your line.
Show fix
Frames point at int(f_str) (library-adjacent) ← to_celsius(raw) ← report("hot"): the bad value entered at the last line. Guard the boundary where outside data comes in:
Module 4 upgrades this to custom exceptions with raise from; Module 6 makes the boundary a CLI that never crashes ugly.
int("hot") "fails" inside int — but int is fine; you handed it "hot". Scroll up past library frames to the deepest line of your own code. That's the fix site, almost every time.Exercise 0.2 — Fix it from the traceback
The box below crashes. Its traceback is printed underneath (as it appears when run). Find the fix site, fix it, rerun clean. Expected final output: average of passing: 75.0.
💡 Hints
- Last line first:
list + intis illegal — line 5 triespassing + s, wheresis one number. - The list method that adds one item is
append. - After the fix, check the math by hand: passing scores are 80, 90, 55 — average 75.0.
Show solution
passing → ZeroDivisionError)? Return 0.0 or a message instead. +feature: also return the count of passing scores. +ship: Module 6 turns this into a CLI flag (--cutoff 50 scores.csv) — keep the function pure so the CLI stays thin.Module 0 done. You know what "intermediate" means, where you stand, where code runs, and how to read a crash. Module 1 starts the real upgrade: replacing loops with readable idioms.
1.1Comprehensions — loops in one line 🟢 Browser-safe
Module 1 replaces hand-written loops with readable idioms. First and most-used: the comprehension — a loop that builds a list, set, or dict in a single expression. Trace this one before running:
The shape is always [expression for item in source] — read it as "give me expression for every item". Same pattern builds the other containers, with an optional filter:
Nested — and when NOT to
Two loops nest left-to-right (same order as writing them out). Use it for genuinely flat transformations — and reach for a plain loop the moment a reader would squint:
append to hunt for. When the transformation doesn't fit one line, that's the comp telling you to write the loop (or a function). Both are respectable; guessing wrong is a style issue, not a bug.Show fix
Output is [1, 2, 3, 4, 5] — a 2 and a 4 survived. After removing the first 2, the second 2 slid into the checked slot and was never examined. Build a new list instead of mutating:
1.2enumerate, zip, sorted — loops that count and pair 🟢 Browser-safe
Need an index? Don't count by hand — enumerate counts for you. Need two lists side by side? zip pairs them. Need order? sorted with a key beats every hand-rolled sort:
sorted(data) hands you a fresh list, data unchanged. data.sort() shuffles data in place and returns None. Default to sorted unless you deliberately want mutation — one fewer surprise.assert len(names) == len(scores) before zipping, or (Python 3.10+) zip(names, scores, strict=True), which raises instead of dropping. Silent data loss is the worst kind of bug — make length mismatches loud.1.3Unpacking — *args and **kwargs 🟢 Browser-safe
Assignment can split a collection apart in one move. The star * soaks up "everything else" — in assignment, in calls, and in function signatures:
def f(a, b, *rest): two required, then any extras as a tuple. def f(**kw): any keywords as a dict. def f(*, flag): everything after a bare * MUST be passed by keyword. You'll read these in every library; Module 6's CLIs write them.1.4Counter, defaultdict, namedtuple 🟢 Browser-safe
Three collections tools that delete whole categories of loop. You tasted Counter in Module 0 — here's the trio doing real grouping and record-keeping:
Show fixes
Last line: KeyError: 'a'. Fix 1 — .get with a default. Fix 2 — stop hand-rolling and use the lesson's tools:
{w: len(w) for w in ["a", "bb", "a"]} keeps only the LAST "a" — no error, data just gone. When keys might repeat, that's a Counter (count them) or a defaultdict(list) (keep them all), never a plain dict comp.namedtuple is the 30-second record: immutable, tuple-compatible, named fields. When you need defaults, validation, or methods, Module 4's @dataclass is the upgrade path — same idea, grown up.1.5Idioms — join, truthiness, any/all 🟢 Browser-safe
The small habits that mark intermediate code: build strings with join, lean on truthiness, ask any/all instead of flag variables:
None, False, 0, "", [], {} are all falsy — everything else is truthy. So if items: means "non-empty", and value or default means "default when missing-ish". Careful: 0 or 5 is 5 — if zero is a valid value, test is None explicitly.1.6Checkpoint — quiz and exercises 🟢 Browser-safe
Five questions, then three builds. The quiz wants 4/5; the exercises want working code.
Exercise 1.1 — Loop to comp (paraphrase)
Rewrite the loop as a single comprehension. Expected: [0, 6, 12, 18].
💡 Hints
- Shape:
[expression for item in source if condition]— three slots, in that order. - Expression triples, condition keeps evens.
Show solution
🚀 Extensions (optional): +feature: also collect the odds tripled into a second comp. +robustness: make it a function triple_evens(nums) that returns [] for an empty input. +ship: time both versions with time.perf_counter on 1M numbers.
Exercise 1.2 — The mystery surcharge (debug)
This till reader crashes on real data. Run it, read the last line, fix the lookup so unknown fruit costs 0. Expected total: 8.
💡 Hints
- Last line first:
KeyError: 'plum'— the comp meets a fruit with no price. - Lesson 1.4's fix:
.getwith a default of 0.
Show solution
🚀 Extensions (optional): +feature: also print a Counter of the basket. +robustness: collect the unknown fruits into a list and print "unpriced: ..." instead of silently charging 0. +ship: read the basket from input() split on commas.
Exercise 1.3 — Top-words reporter (build)
Finish the reporter: normalize the text, count, show the top n. ~25 lines when done.
💡 Hints
TEXT.lower(), then loop the punctuation chars replacing each with""— or chain.replace(".", "")calls.Counter(words).most_common(2)gives[("apple", 4), ("banana", 2)]— unpack in a loop to print pretty lines.
Show solution
🚀 Extensions (optional): +feature: take n from the user (guard the int() like Module 0). +robustness: skip words shorter than 3 letters. +ship: this exact core returns inside Capstone B — keep it pure (no input inside the counting).
2.1First-class functions — plus lambda discipline 🟢 Browser-safe
First-class means functions are ordinary values: store them in dicts, pass them as arguments, return them from functions. The moment you stop seeing def as special syntax and start seeing functions as things you can hand around, a shelf of techniques opens:
And the tiny anonymous sibling — lambda makes a one-expression function with no name. Discipline matters more than syntax here:
if/elif chains (commands, menu handlers, file-format readers). Passing key= functions customizes sorted/min/max without rewriting them. Returning functions builds factories (next lesson). Every decorator and callback in Modules 6–8 is just this idea, dressed up.lambda x: a if c else b), or needed twice → def. Linters flag assigned lambdas (f = lambda: ...) — that's just a def with extra steps.2.2Closures — functions that remember 🟢 Browser-safe
A closure is a function that remembers variables from where it was born — even after that scope finished. State without a class, without globals:
i — the variable, not its value. By call time the loop is done. Predict, run, watch [0, 1, 2] not appear.
Show fix
Output is [2, 2, 2] — all three read i after the loop left it at 2. Freeze each value with a default argument (defaults evaluate at def time):
def runs — not per call. A list default becomes one list shared by every call. The first call looks fine; the second reveals the haunting.
Show fix
Sentinel idiom: default to None, build fresh inside. (Immutable defaults — numbers, strings, tuples — are always safe.)
2.3map/filter, itertools, functools 🟢 Browser-safe
map/filter apply a function to a pile — but after Module 1 you already own the clearer spelling. Rule: existing named function and no lambda needed → map is fine; otherwise the comp reads better:
itertools is the standard pile-plumbing kit. Three members cover 90% of uses — islice (take some, lazily), chain (one stream from many), groupby (runs of equal keys):
sorted(data, key=key) first with the same key (as above). If the input can't be sorted, accumulate into a defaultdict(list) (Lesson 1.4) instead.And functools: lru_cache memorizes pure functions, partial freezes some arguments to mint specialists. Note the spelling — calling lru_cache(...) on a function, no @ needed (that syntax is Module 7; this is the same machinery):
TypeError: unhashable type. Tuples instead of lists at cached boundaries; keep I/O out.2.4Checkpoint — quiz and exercises 🟢 Browser-safe
Five questions, then three builds. Quiz wants 4/5.
Exercise 2.1 — Chain to dict (paraphrase)
Replace the if/elif chain with a dispatch dict (Lesson 2.1). Behavior must stay identical.
💡 Hints
- Define
add/mul/pwhelpers (or reuse operators) and map names to them:ops = {"add": ..., ...}. - Then
runis one line:return ops[op](a, b). Unknown op?KeyError— honest, like Lesson 1.4.
Show solution
🚀 Extensions (optional): +feature: add "sub". +robustness: raise a friendly ValueError("unknown op: ...") for bad names (Module 4 polishes this). +ship: read op a b from input().split().
Exercise 2.2 — Three handlers, one bug (debug)
Should print handler 0, handler 1, handler 2 — prints one name thrice. Late binding (Lesson 2.2); freeze it.
💡 Hints
- Same trap as Crash lab 1: all three read
iafter the loop. - Freeze with a default:
lambda i=i: ....
Show solution
🚀 Extensions (optional): +feature: store handlers in a dict keyed by name. +robustness: write a make_handler(i) factory (closure, no defaults trick). +ship: dispatch table of real commands (see 2.1).
Exercise 2.3 — Ranked leaderboard (build)
Finish the ranker: sort by score descending, medal the top 3, print a table. ~20 lines when done.
💡 Hints
key=lambda p: p[1], reverse=True— Python's sort is stable, so the 95-tie keeps input order.for rank, (name, score) in enumerate(ordered, start=1):unpacks two levels at once.
Show solution
🚀 Extensions (optional): +feature: medal emojis for ranks 1–3. +robustness: handle the empty-players case. +ship: Capstone B ranks reporters this way — keep the sort key a named function.
3.1The iterator protocol — iter, next, done 🟢 Browser-safe
Every for loop you've ever written is sugar for three steps: iter() gets an iterator, next() pulls values, StopIteration ends it. Two roles: an iterable can make fresh iterators (lists, strings, files); an iterator is single-use and stateful:
The protocol in ten lines — __iter__ returns an iterator, __next__ returns values until it raises StopIteration (Module 4 explains dunders fully; here just watch the machinery):
Show fix
Keep the re-usable iterable around and call iter() per pass — or materialize one list if it fits memory:
3.2yield — ever-pausing functions 🟢 Browser-safe
A function with yield doesn't run when called — it returns a generator, paused at the top. Each next() resumes after the last yield and pauses at the next one. Nothing is computed until asked:
Pause-forever means infinite sequences are fine — as long as the consumer takes only what it needs. itertools.islice (Lesson 2.3) is the guard rail. This box is safe because of that last line:
list(evens()) hangs forever building a list that never ends — same frozen tab as Module 0's while True, same cure (reload; code is saved). Rule: infinite sources only ever feed islice, counters, or break-guarded loops. True infinites appear in this course only behind such guards.One generator delegating to another uses yield from — "yield everything this produces", including cleaning up after it:
Show fix
Call the function again for a fresh generator — nums() is the factory, g was one product:
3.3Genexps and lazy pipelines 🟢 Browser-safe
Parentheses instead of brackets make a comprehension lazy — a genexp that yields values on demand and never builds the list. Same syntax, ~100x less memory:
The payoff: chain small generators into a pipeline — each stage transforms the stream, nothing lands in memory whole. Here with an in-memory stand-in for a file (Module 5 brings real ones):
Bridge to Module 7: generators with cleanup run their finally when close()d — even if the consumer quits early. contextlib.closing pairs them with with:
islice(stage, 3) to peek without consuming the world. Naming a stage well (errors, not filter2) is half the readability.3.4Checkpoint — quiz and exercises 🟢 Browser-safe
Five questions, then three builds. Quiz wants 4/5.
Exercise 3.1 — Eager to lazy (paraphrase)
Same answer, constant memory: convert the list-building version to a genexp. Both print 332833500.
💡 Hints
sum(...)takes any iterable — brackets to parens, drop the list.- Verify with
sys.getsizeofif curious (Lesson 3.3).
Show solution
🚀 Extensions (optional): +feature: parameterize the limit. +robustness: prove laziness with a generator that prints when resumed (Lesson 3.2). +ship: sum a 10M range both ways and compare peak memory qualitatively.
Exercise 3.2 — Min after max (debug)
Should print max 9 then min 1 — crashes instead. One variable, used twice, single-use: fix it two ways.
💡 Hints
maxconsumed the iterator — Crash labs 1–2, same disease.- Fix A: keep the reusable
[5, 1, 9, 3]list, iterate twice. Fix B:data = list(...)once, reuse the list.
Show solution
🚀 Extensions (optional): +feature: also print the mean. +robustness: handle the empty-input case (both raise ValueError — catch it or default it). +ship: min/max in ONE pass with a loop (two passes waste streams you can't rewind).
Exercise 3.3 — Batched chunks (build)
Finish batches(): yield successive lists of size n from any iterable. Last batch may be short. ~15 lines when done.
💡 Hints
batch = list(islice(it, n))— thenif not batch: return, elseyield batch, in awhile True.returninside a generator just stops it (no value — that part is Module 7-adjacent, ignore for now).
Show solution
🚀 Extensions (optional): +feature: pad the last batch with None instead of shortening. +robustness: reject n < 1 loudly. +ship: Capstone B batches DB inserts this way — keep it generic over any iterable.
4.1@dataclass — records without boilerplate 🟢 Browser-safe
Most classes are records: named fields, equality by value, readable repr. Hand-writing __init__ + __repr__ + __eq__ for every record is toil — @dataclass generates it from the field list:
Defaults are fine — except mutables, which need a factory. Plus frozen=True for immutable records:
items: list = [] inside a dataclass raises ValueError: mutable default ... use default_factory at class-creation time — the same shared-list haunting as Lesson 2.2's crash lab, but caught immediately instead of in production. field(default_factory=list) (or dict, set) is the only spelling.4.2Dunders that pay — str, repr, eq, len 🟢 Browser-safe
Python has ~100 dunders; four earn their keep weekly. __str__ is for humans (print), __repr__ for developers (debuggers, containers, logs) — ideally an unambiguous echo of construction:
__len__ unlocks len() (and truthiness: empty means falsy). __eq__ compares by data — but it quietly disables hashing:
__eq__ sets __hash__ to None — Python assumes mutable-by-value objects shouldn't hash. Sets and dict keys then explode. Predict which line dies.
Show fix
Last line: TypeError: unhashable type. If equal objects are truly interchangeable, hash from the same fields __eq__ compares:
Rule: __eq__ without __hash__ means "unhashable, keep out of sets". Fine for mutable entities — fatal surprise for value objects. (Dataclasses: eq=True, frozen=True generates both.)
4.3Composition over inheritance 🟢 Browser-safe
Two ways to reuse behavior. Inheritance (IS-A): a subclass reuses and overrides. Composition (HAS-A): an object holds helpers and delegates. Inheritance is taught first and reached for most — composition wins more often. The tell: new behavior as a new subclass per combination (DiscountedCart, TaxedCart, DiscountedTaxedCart…) versus a policy plugged into one class:
Inheritance earns its place for shared interfaces with substitution: an Admin IS-A User wherever login code expects a User, honoring the same method contracts. Foundations' Student(Person) qualifies — a student can stand in for a person. A cart with discount options doesn't: that's configuration wearing a subclass costume. Heuristic: reach for composition first; inherit only when callers should treat the child as the parent.
4.4@property — validated attributes 🟢 Browser-safe
Plain attributes accept anything (score.points = -40 — sure, why not). @property keeps the obj.attr syntax but routes access through methods — validation at the boundary, with storage in a private backing field:
self.points = ... re-invokes the setter — assignment to a property is a setter call, forever. Same for the getter reading self.points.
Show fix
Last line: RecursionError. Property methods must touch the backing field (_x), never the property name:
4.5Custom exceptions — raise from 🟢 Browser-safe
Domain errors deserve domain types: callers catch the meaning (OverdraftError), not ValueError soup they can't distinguish from bugs. And raise ... from ... chains the cause, so tracebacks tell the full story:
except Exception: (and its worse sibling, bare except:) catches typos, logic errors, and typos-in-error-handling alongside the error you meant. Catch the specific exceptions a block can honestly produce (ValueError at input boundaries); let everything else crash loudly where you'll see it. Module 6's logging gives the third option: record-and-continue, deliberately.except around a typo returns a confident wrong answer — no traceback, no clue, just 0 where 30 belongs.
Show fix
No exception is expected here, so no try belongs here at all — delete the net and let typos crash in daylight:
4.6Checkpoint — quiz, lab, exercises 🟢 Browser-safe
Five questions, one refactor lab, two exercises. Quiz wants 4/5; the lab is the real exam.
Lab 4 — Refactor the Bank (build)
Foundations' bank account, grown up: a dataclass with validated balance, domain errors, and a move log. Build order:
OverdraftError(Exception)— one line, domain type.@dataclass Accountwithowner: strandlogviadefault_factory;__post_init__opens the log.balanceproperty + setter validating0..1000000on a_balancefield.deposit/withdrawmethods that log("in"/"out", amount); withdraw raises before logging.__str__as"ada: 75 (2 moves)".
💡 Hints
- Property + dataclass mix freely: the property is a class attribute, not a field (no annotation → not a field).
- Withdraw order: check funds (raise) → set balance → log. Log only completed moves.
moves = len(self.log) - 1(the "open" entry isn't a move).
Show solution
🚀 Extensions (optional): +feature: a transfer(other, amount) method (withdraw + deposit, one log each). +robustness: reject negative deposits loudly. +ship: this account returns in Capstone A — keep I/O out of it.
Exercise 4.1 — Dicts to dataclass (paraphrase)
Player records as dicts work until something misspells a key. Convert to a dataclass; behavior identical, typos impossible.
💡 Hints
@dataclass class Player: name: str; score: int = 0— then attribute access replaces every["..."].- A misspelled attribute now crashes (good!) instead of silently adding a key.
Show solution
🚀 Extensions (optional): +feature: add a level computed from score. +robustness: validate non-negative score via property. +ship: sort a roster with Lesson 2.1's key functions.
Exercise 4.2 — Unhashable roster (debug)
Should print 2 unique — dies on the set instead. One dunder missing; add it.
💡 Hints
- Last line first:
TypeError: unhashable type: 'Player'—__eq__nulled the hash. def __hash__(self): return hash(self.name).
Show solution
🚀 Extensions (optional): +feature: add __repr__ so the set prints readably. +robustness: what if two players share a name but differ elsewhere — is name-hash still honest? +ship: rewrite as a frozen dataclass (both dunders free).
5.1pathlib — paths as objects 🟡 Browser-adapted
🟡 Browser-adapted: identical code both places — but this tab's files live in memory and vanish on close, while your PC keeps them in ~/.pyintermediate/workspace/ (desktop) or your project folder (terminal).
Stop gluing strings with + "/" +. A Path joins with /, knows its own name/suffix, and reads/writes in one call. This module's arc: a garden journal stored as date|text lines, upgraded lesson by lesson to JSON, then SQLite:
open("day1.txt") looks in the current working directory (where you ran), not beside your script. Different launch folders, different crashes — the classic "works on my machine".
Show fix
Last line: FileNotFoundError. Foundations habit — expect the miss, and anchor to a known directory instead of vibes:
5.2csv — commas are trickier than they look 🟡 Browser-adapted
🟡 Browser-adapted: same code, same output — on your PC the file persists for the next lesson; here, rerun the writer if you jumped in mid-module.
"rain, no watering".split(",") gives three fields from two — quoted commas, embedded newlines, and quotes defeat hand-splitting. The csv module handles all of it, including our pipe-delimited journal via delimiter:
newline="", Python translates line endings on write and the csv reader sees phantom blank rows — on Windows first, everywhere eventually. Both open() calls above carry it; make it muscle memory for every CSV you touch. Same for encoding="utf-8" once names carry accents.5.3json — the lingua franca 🟡 Browser-adapted
🟡 Browser-adapted: same JSON everywhere — but fetching it differs: this tab uses browser-native pyfetch, your PC uses requests. The parsing below is identical.
JSON is how programs trade data: dicts/lists ↔ text. dumps/loads work with strings, dump/load with files. Our journal, upgraded from pipes to structure:
5.4sqlite3 — a database in one file 🟡 Browser-adapted
🟡 Browser-adapted: :memory: databases behave identically everywhere. For persistence, swap in a filename — on your PC it becomes a real .db file; here, download it via the snippet in the prose.
SQLite is a full SQL database in a library — no server, one file, zero setup. Same journal, third upgrade: queryable rows instead of scanned text. Two non-negotiables: ? placeholders (never f-strings — injection and quoting bugs) and commit():
Persistence note: replace ":memory:" with "journal.db" and the database outlives the session — on desktop it lands in ~/.pyintermediate/workspace/; in this tab, files vanish on close, so treat the browser as the rehearsal and your PC as the archive.
5.5Cleaning dirty data — regex and dates 🟢 Browser-safe
Real data arrives filthy: ragged spacing, N/A where dates belong, quotes inside quotes. Two tools carry the cleaning shift — re for shape, datetime for time. The essentials, no more: search (anywhere), match (start only), findall (every hit):
.* matches as much as possible: first quote to LAST quote, swallowing the middle. Not a crash — silently wrong results, the regex rite of passage.
Show fix
Output: ['hi" then "bye'] — one giant match. Fix with lazy .*? (as little as possible), or better, a negated class that can't cross quotes:
Dates: strptime parses, isoformat stores — and naive datetimes (no zone) refuse to compare with aware ones. Parse, then stamp a zone immediately:
Show fix
Last line: TypeError on comparison. Rule: parse → zone immediately → store ISO. Assume UTC unless a zone is given:
N/A date poisons every sort, average, and chart after it. The cleaning pass (parse → validate → normalize-or-reject) belongs at the door — every loader in Lessons 5.2–5.4 should reject or quarantine bad rows before they enter. Capstone B grades this.5.6Checkpoint — quiz and exercises 🟢 Browser-safe
Five questions, then three builds. Quiz wants 4/5.
Exercise 5.1 — split breaks, csv doesn't (paraphrase)
The hand-split drops a field on quoted commas. Replace it with csv.reader; all three fields must survive.
💡 Hints
list(csv.reader(io.StringIO(line)))[0]— one row, three fields.io.StringIOturns a string into a file-like object (Lesson 3.3's trick).
Show solution
🚀 Extensions (optional): +feature: parse three such lines with DictReader and a header. +robustness: handle a ragged line (2 fields) by reporting it, not crashing. +ship: this is Capstone B's front door — keep the reader separate from the analysis.
Exercise 5.2 — load vs loads (debug)
Should print {'a': 1} — raises AttributeError instead. One letter fixes it.
💡 Hints
- Last line:
AttributeError: 'str' object has no attribute 'read'—loadtried to read a file. - Rule of thumb:
loads← string in memory,load← open file handle.
Show solution
🚀 Extensions (optional): +feature: pretty-print with indent=2. +robustness: catch json.JSONDecodeError on bad input (it's a ValueError subclass — Lesson 4.5). +ship: read the JSON from a real file with json.load(open(...)) — better, with open.
Exercise 5.3 — Journal loader (build)
Finish the loader: date|text lines in, validated entry dicts out, bad lines reported (not crashed on). ~25 lines when done.
💡 Hints
line.split("|", 1)— maxsplit 1, so pipes inside text survive.try: datetime.strptime(...) except ValueError: print("SKIP:", line); continue— validate at ingestion (Lesson 5.5).- Store
dt.date().isoformat()— canonical, sortable, SQLite-friendly.
Show solution
🚀 Extensions (optional): +feature: also reject empty text. +robustness: collect skips into a list and report a summary count. +ship: feed these entries straight into Lesson 5.4's INSERT — that pipeline IS Capstone B's core.
6.1Project setup — layout, venv, pip 🟡 Browser-adapted
🟡 Browser-adapted: concepts + transcripts here; the one runnable box uses the browser's micropip (desktop shows it as reference). Real venv/pip happen in your terminal, inside the pywork folder from Lesson 0.3.
Professional layout, full tour — one folder per program, tests beside code, dependencies written down:
In this tab there is no pip — but Pyodide ships micropip, real packages from real PyPI (internet needed). Same packages your terminal gets:
pip freeze > requirements.txt after every install; pip install -r requirements.txt resurrects the environment anywhere. "Latest" today is broken tomorrow — exact versions are how six-months-later-you (and your teammates) reproduce a working setup.6.2argparse — programs with a command line 🟢 Browser-safe
Buttons are for browsers; real tools take arguments. argparse turns a function call into a CLI with --help, types, choices, and defaults — free. The browser has no command line, so lessons use an ARGS stand-in (desktop/terminal call parse_args() bare for the real thing):
Types convert, choices reject, and rejections raise SystemExit (not a normal exception — it carries the process exit code):
6.3logging — prints with levels 🟢 Browser-safe
print debugging litters shipped code with chatter you can't switch off. logging gives every message a level — one dial (basicConfig(level=...)) decides what shows, per run, without touching code:
WARNING: your INFO dies quietly. ("But my warning printed?" — yes: WARNING+ squeaks through a last-resort handler. INFO just dies.)
Show fix
Only end of program printed. One line configures the dial — then the message flows:
6.4pytest — tests that guard your back 🔴 Terminal-required
🔴 Terminal-required: pytest runs on your PC (python -m pytest in the venv). This lesson's runnable boxes are the code under test (plain asserts run anywhere); the pytest runs themselves are transcripts — a passing run is never faked in-browser.
The whole framework in one habit: write the expectation as a bare assert in a test_*.py file, run, read failures. Start with the code — assertions hold here exactly as they hold under pytest:
Same assertions, dressed as a test file, run for real:
Read failures like tracebacks: the > line is the expectation, E lines are the evidence (1000000.0 == 1000000 — a float/int strictness lesson for free). Now the test-writing trap:
Show fix
tmp_path — pytest hands every test a fresh, private temp directory. Hermetic: order-proof, CWD-proof, parallel-safe:
breakpoint() drops into pdb waiting on a human — in CI the run hangs until timeout; for users, the program just stops. It never belongs in committed code: write the test instead of the breakpoint, and sweep before pushing:
pdb commands for the terminal when you do stop deliberately: next line, print a variable, continue, quit. But default to tests — breakpoints don't regress.6.5Checkpoint — quiz and exercises 🟢 Browser-safe
Five questions, then three builds. Quiz wants 4/5.
Exercise 6.1 — Prints to levels (paraphrase)
Same output lines, grown-up plumbing: route each print through the matching level. Keep the format from Lesson 6.3.
💡 Hints
log = logging.getLogger("ex61"), thenlog.info/debug/warning(...)per line.- "trace detail" must VANISH (debug < INFO) — that's the test.
Show solution
🚀 Extensions (optional): +feature: add --verbose (argparse, Lesson 6.2) switching INFO to DEBUG. +robustness: log to a file too (filename="run.log"). +ship: every Capstone logs instead of printing — convert one now.
Exercise 6.2 — Read the failure (debug)
pytest says no. The transcript below is real output against the box's code — find the +1 that shouldn't be there.
💡 Hints
E assert 4.0 == 3: the function returns 4.0, the test wants 3.sum([2, 4]) / len([2, 4])is already 3 — what does the+ 1do?
Show solution
🚀 Extensions (optional): +feature: rename to average and guard empty input. +robustness: add the empty-raises test from Lesson 6.4. +ship: run it under real pytest in your venv — green feels different when it's yours.
Exercise 6.3 — Logged greeting CLI (build)
Finish the CLI: ARGS shim, --upper flag, logging instead of prints. ~20 lines when done.
💡 Hints
- Lesson 6.2's parser, Lesson 6.3's logger — bolt them together, thin
main()optional. args = parser.parse_args(ARGS), then onelog.info(...).
Show solution
🚀 Extensions (optional): +feature: add --count (Lesson 6.2). +robustness: use the real ARGS variable, not the literal list. +ship: this is Capstone A's skeleton — save it to main.py in your venv.
7.1Decorators — functions that upgrade functions 🟢 Browser-safe
After Modules 2 (first-class functions) and 3 (pausing functions), the power tools unlock. A decorator is Lesson 2.1 grown up: a function that takes a function and returns an enhanced one. The @ is pure sugar — @timer above a def just means "reassign the name through timer":
Decorators take arguments via a factory (a function returning the decorator), and they stack bottom-up — the closest one runs first:
@wraps, every decorated function reports the wrapper's name and loses its docstring — pytest shows wrapper, logs lie, help() goes blank. Not a crash: identity theft.
Show fix
One line: @wraps(func) copies name, docstring, and module onto the wrapper. Non-negotiable in every decorator you write:
@a @b def f means f = a(b(f)): b wraps first, a sees b's output. Order bugs show as "my timing includes the retry waits" (timer outside retry) vs "each attempt timed separately" (timer inside) — both valid, pick deliberately.7.2Context managers — setup, use, guaranteed cleanup 🟢 Browser-safe
with open(...) was your first context manager: setup, use, guaranteed cleanup. Custom ones come in two spellings — the class (__enter__/__exit__) and the generator (@contextmanager, pure Module 3):
__exit__'s return value is a loaded gun: truthy means suppress the exception. Return True by accident (or "helpfulness") and errors vanish mid-program — the except Exception: pass of Lesson 4.5, wearing a tuxedo.
Show fix
Return False (or None — the default) unless you are deliberately implementing suppression (like contextlib.suppress, which says so in its name):
7.3Combined — timed, retried, logged 🟢 Browser-safe
The graduation exercise: both tools on one job. Small composable pieces — a @retry decorator, a timed context — around a flaky operation (simulated: no network needed). Notice the layers stay independent and testable:
@retry_time_log hydra nobody can test. Same rule as functions (one job) and pipeline stages (Lesson 3.3) — it keeps recurring because it keeps working. Capstone C leans on exactly this shape.Exercise 7.1 — Logged retry (build)
Finish the decorator: retry with a log line per failure, giving up loudly after tries. ~15 lines when done.
💡 Hints
- Lesson 7.1's retry, plus
log = logging.getLogger("retry")and onelog.warning(...)inside theexcept. - Keep
last = errper failure;raise lastafter the loop (bareraiseoutside anexceptis illegal).
Show solution
🚀 Extensions (optional): +feature: back off (sleep attempt seconds between tries). +robustness: retry only chosen exception types (a parameter). +ship: Capstone C wraps its fetch exactly like this — lift it whole.
Modules complete. Three capstones remain: a CLI file tool, a CSV→SQLite reporter, and a tested mini-API — each one a 200–400-line portfolio program built from everything above.
8.1Capstone A — CLI file tool (jstat) 🔴 Terminal-required
🔴 Terminal-required (with 🟢 flattened preview below — the whole tool in one runnable box; the real thing is three files you build on your PC).
Spec. jstat reads a garden journal (date|text lines, Module 5) and reports: entry count, distinct words, top-N words. python main.py journal.txt --top 3 --verbose must exit 0 and log its steps; missing file exits 2 with a clean error, never a traceback.
| Behavior | Example |
|---|---|
main.py FILE | prints entries / distinct / top-1 |
--top N | top-N words with counts |
--verbose | DEBUG logging on |
| missing file | file not found: ... on stderr, exit 2 |
Build order.
journal.py— pureload+stats(no I/O inside; testable!).tests/test_journal.py— three asserts; runpython -m pytestafter every step.main.py— thin shell: argparse, logging, file read, print report.- Break it: empty file, missing file, 10k-line file. Fix, extend (below).
💡 Hints
main.pymirrors Exercise 6.3: parse, configure logging (verbose → DEBUG),try: Path(file).read_text() except FileNotFoundError→parser.error(...)(exits 2 cleanly).- Empty file:
most_common(1)on an empty Counter raisesIndexError— guard it (return top"—", count 0). - Keep
journal.pyfree ofinput/print/open— purity is what makes it testable.
Show solution (three files)
🚀 Extensions (optional): +feature: --by-date flag printing entries-per-day (Counter over dates). +robustness: skip malformed lines with a warning (Lesson 5.5). +ship: pip install -e . + a console entry point so jstat runs bare.
most_common(1)[0] on nothing is an IndexError. Edge cases first, always.
Show fix
Guard the empty case where the data is born — the solution's stats() already does: top, count = words.most_common(1)[0] if words else ("—", 0):
8.2Capstone B — CSV to SQLite reporter 🔴 Terminal-required
🔴 Terminal-required (with 🟢 flattened preview below — the pipeline in one runnable box; the real thing is three files + a real .db on your PC).
Spec. sales.csv (date,product,qty,price) in, revenue report out: per-product revenue ranked, top day overall. Dirty rows (bad numbers, missing fields) are skipped with a warning, never fatal. The DB file persists between runs.
Build order.
report.py— pureclean+totals(dicts in, dicts out).tests/test_report.py— dirty-row skip, revenue math, empty input.main.py— argv (in-csv, out-db), logging, load → SQLite → print.- Feed it 10k generated rows; watch it stay flat in memory (generators, Lesson 3.3).
💡 Hints
cleanraises, the loader catches (validate at ingestion, Lesson 5.5):try/except (ValueError, KeyError)→log.warning("skip row %d", n).- Revenue SQL:
SELECT product, SUM(qty * price) AS revenue ... GROUP BY product ORDER BY revenue DESC; top day: same grouped bydate,LIMIT 1. - Batch inserts with
Lesson 3.3's batches()for the 10k-row stretch goal.
Show solution (three files)
🚀 Extensions (optional): +feature: --by-day full daily table. +robustness: stream the CSV with generators (never list(reader)). +ship: argparse.FileType vs manual open — compare error messages.
O'Brien) — and invites injection. Placeholders never blink.
Show fix
Last line: OperationalError: near "brien": syntax error — the quote ended the string early. ? keeps code and data apart (the solution's loader already does):
8.3Capstone C — tested mini-API 🔴 Terminal-required
🔴 Terminal-required (with 🟢 flattened preview below — the pure routing core runs here; sockets need your PC).
Spec. A JSON API over http.server (stdlib only): GET /players lists, GET /players/NAME fetches, unknown → 404 JSON. The routing core is pure ((method, path) → (status, body)) and pytest-tested; the HTTP shell is thin.
Build order.
api.py— pureroute(), no sockets, no globals mutated in surprising ways.tests/test_api.py— list, fetch, 404s; green before any networking.server.py— 30-lineBaseHTTPRequestHandlershell callingroute.curlevery route; addPOSTas the extension.
💡 Hints
path.strip("/").split("/")then match shapes:["players"]vs["players", name]— Lesson 1.3 unpacking does the reading.- Unknown player and unknown route are DIFFERENT 404 bodies — tests pin both.
server.py:json.dumps(body).encode(),Content-Type: application/json,Content-Length— thenHTTPServer(("127.0.0.1", 8000)).serve_forever().
Show solution (three files)
🚀 Extensions (optional): +feature: POST /players with a JSON body (parse Content-Length, validate, 201). +robustness: log every request with Lesson 6.3 levels (INFO hits, WARNING 404s). +ship: persist STORE to SQLite (Lesson 5.4) so players survive restarts.
AAppendix A — regex quick reference 🟢 Browser-safe
Everything Lesson 5.5 taught, on one screen. Static reference — nothing to run.
| Token | Matches | Example |
|---|---|---|
\d \w \s | digit, word char, whitespace | \d{4}-\d{2}-\d{2} a date |
. | any char (except newline) | a.c matches "abc", "a-c" |
* + ? | 0+, 1+, 0-or-1 of the previous | \d+ one or more digits |
{m,n} | between m and n | \w{2,8} short words |
[...] [^...] | any of / any but | [^"]* "no quotes" (the pro move) |
(...) | | capture group / either-or | (cat|dog)s? |
^ $ | string start / end | ^\d+$ "only digits, whole string" |
.*? | lazy version — as LITTLE as possible | "(.*?)" quoted fields |
BAppendix B — itertools cheat sheet 🟢 Browser-safe
Lessons 2.3 and 3.3 introduced the big three; the rest of the kit, same lazy deal — everything returns iterators, nothing builds lists. Static reference.
| Tool | Does | Example → result |
|---|---|---|
islice(it, n) | first n items | islice(count(), 3) → 0, 1, 2 |
chain(a, b) | one stream from many | chain("ab", [1]) → a, b, 1 |
groupby(data, key) | runs of equal keys (sort first!) | Lesson 2.3's grades |
product(a, b) | cartesian pairs | product("AB", "12") → A1 A2 B1 B2 |
permutations(xs, n) | orderings | permutations("ABC", 2) → 6 pairs |
combinations(xs, n) | selections, order ignored | combinations("ABC", 2) → 3 pairs |
repeat(x, n) | x, n times | repeat(0, 3) → 0, 0, 0 |
cycle(xs) | repeat forever (guard with islice!) | islice(cycle("AB"), 5) |
count(n, step) | infinite counter | enumerate with any start/step |
accumulate(xs) | running totals | accumulate([1,2,3,4]) → 1, 3, 6, 10 |
zip_longest(a, b) | zip without dropping (fillvalue) | the anti-truncation zip |
CAppendix C — field guide and where next 🟢 Browser-safe
The errors you'll meet weekly, the commands you'll type daily, and the roads after this course. Static reference.
Error → meaning → fix
| Last line | Means | Fix (lesson) |
|---|---|---|
NameError | typo or wrong scope | read the name, check spelling (0.4) |
TypeError | right shape, wrong type | convert, or guard the boundary (4.5) |
KeyError / IndexError | missing member | .get / length check / guard empties (1.4, 8.1) |
ValueError | right type, bad value | validate at ingestion (5.5) |
AttributeError | object lacks that name | check type, check spelling (5.6: load/loads) |
FileNotFoundError | wrong folder assumed | anchor to a known dir (5.1) |
RecursionError | self-calling property/function | backing field, base case (4.4) |
StopIteration (raw) | manual next() past end | let for handle it (3.1) |
unhashable type | __eq__ without __hash__ | hash what you compare (4.2) |
Terminal daily drivers
Where next — three lanes
| Lane | Build next | Learn next |
|---|---|---|
| Web | serve Capstone C properly; forms + templates | HTTP deeply, then one framework (Flask/FastAPI/Django) |
| Data | 10k-row reporters; charts from Capstone B | SQL deeply, then pandas |
| Automation | scheduled jstat-style tools; backups | argparse mastery, packaging, cron/systemd |
Scope, one last time: async mastery, metaclasses/descriptors, production web, pandas/numpy tracks, ML, and PyPI publishing are beyond this course by design — each lane above teaches them in its own good time. What this course promised was 200–400-line tested programs from stdlib + pytest. You have three of those now. Go build the fourth.