Workshop · Python · Advanced
PY300 Python, advanced: services, concurrency and proof
The advanced Python course is one service built eight ways deeper: the Orders processor. You lay out its layers and configuration, add concurrency with threads and asyncio under deadlines, persist through a repository over sqlite3 with hand-written migrations, expose it over an HTTP API written on http.server, test it at every level including golden and property-style tests, profile and speed it up, review a planted-defect module and verify machine-written code, and finish with a capstone the grader exercises with hidden files and hidden tests.
Open Coding Tutor at codingtutor.agilesystemsoftware.com, pick PY300 from the course button in the chat and press Tutor me. Every lesson, demo and hands-on step is also published below.
Who it is for
Working developers who write Python daily and want to build and defend a small production-shaped service: layered, configured, concurrent where it pays, persisted, exposed over HTTP, measured, reviewed, and proven by tests on inputs they did not write. Everything is standard library, so the ideas transfer to any framework you meet later.
What you will be able to do
- Lay out a service in layers with dependencies pointing inward, configuration from the environment and structured logs
- Choose between threads, processes and asyncio, run work concurrently under timeouts, and keep shared state safe
- Put storage behind a repository, write migrations and transactions by hand over sqlite3, and test with a fake
- Serve and consume an HTTP JSON API with correct status codes, validation at the edge and in-process tests
- Build a test suite with fakes, integration, golden and property-style tests, and a coverage floor
- Measure with timeit, cProfile and tracemalloc, reason about complexity, and record a before/after proof
- Read an unfamiliar module, find defects by review, write the review, and verify machine-generated code with tests
- Deliver a production-shaped batch service with an exit-code contract that survives inputs you did not see
Syllabus
Every session ends in a lab the machine checks on your own code. Open a session's mini steps to read each lesson, demo and hands-on task; the badge shows how far the engine can verify it: checked · T3 a check runs over your files, proven · T4 the grader also tries inputs you did not see, tutored · T0 done on your word. Each step has its own link (for example #s1-1).
- 1. Architecture of a service
- 2. Concurrency
- 3. Persistence
- 4. An HTTP API on the standard library
- 5. Testing at scale
- 6. Performance
- 7. Reading, reviewing and the AI era
- 8. Capstone: Orders
Session 1 · Architecture of a service
Six steps that give the Orders service its shape before it does anything. Layers: domain in the middle, application services around it, adapters (files, database, HTTP, CLI) at the edge, dependencies pointing inward. Protocols as the contracts adapters implement. Configuration as a frozen Settings dataclass built from environment variables and arguments, with secrets never in source. Logging as an adapter emitting one JSON object per event. The application service returning a Result rather than printing. Live in the seat: an import graph drawn from the code, a Protocol satisfied by two classes, a missing environment variable failing fast, and a walking skeleton that reads a file and reports counts.
Lab Build the walking skeleton of Orders: the package with domain, app, adapters and cli layers, Settings from the environment, JSON logging, and a process command that reads orders.json and reports how many orders it found, exit 0, or exit 8 on an unreadable file. Session 1 checkpoint.
6 mini steps · ~53 min
1.1Layers and the direction of dependencies9 minchecked · T3
Predict
orders/domain/pricing.py imports sqlite3 to look up a discount. Which rule is broken?
Lesson
Four layers: domain (entities, rules, pure functions), app (use cases orchestrating the domain through interfaces), adapters (sqlite, files, HTTP, CLI implementing those interfaces) and the composition root that wires them. Imports go inward only; the domain imports nothing of yours.
Demo
The tutor lays out python/py300/demo/svc/{domain,app,adapters,cli}/ with one module each, runs a tiny import-graph script over it, then adds a forbidden import and shows the script flag it.
Hands-on
Create python/py300/s1/orders/ with domain/__init__.py, app/__init__.py, adapters/__init__.py, cli/__init__.py and a module docstring in each stating what may import what. Write s1/check_layers.py that walks the package with ast, finds imports between layers and prints "layers ok" or the violation.
Explain
Why must the domain import nothing from the other layers?
Check
The layered package exists and check_layers prints layers ok — checked (T3):
seat_fileseat_fileseat_run1.2Protocols: contracts for adapters9 minchecked · T3
Predict
class OrderSource(Protocol): def read(self) -> list[dict]: … A FileSource with a matching read() but no inheritance — does basedpyright accept it as an OrderSource?
Lesson
typing.Protocol declares the methods a dependency must have. The app layer depends on the Protocol; adapters implement it by shape; tests pass a fake. Keep protocols small — one or two methods — and name them for the role (OrderSource, ReportSink), not the technology.
Demo
In python/py300/demo/svc/ the tutor writes OrderSource and ReportSink protocols in app/ports.py, a FileSource adapter and an InMemorySource fake, and shows basedpyright accepting both and rejecting one with a wrong return type.
Hands-on
Write s1/orders/app/ports.py with Protocols OrderSource (read() -> list[dict]) and ReportSink (write(text: str) -> None); s1/orders/adapters/files.py with JsonFileSource(path) and TextFileSink(path) implementing them; s1/ports_demo.py uses both to copy a count line into s1/out/report.txt. basedpyright clean.
Explain
Why name a protocol for its role instead of its technology?
Check
Ports are Protocols and the file adapters satisfy them — checked (T3):
seat_fileseat_lintseat_run1.3Configuration without secrets8 minchecked · T3
Predict
ORDERS_DB is unset and the code does os.environ["ORDERS_DB"]. What happens, and is it good?
Lesson
Settings is a frozen dataclass built once at startup by Settings.from_env(environ, argv): required variables fail fast with a clear message, optional ones have typed defaults, and nothing secret is ever written in source or printed in logs. Everything downstream receives Settings, never reads the environment.
Demo
The tutor builds Settings in python/py300/demo/svc/app/settings.py, runs with the variable missing to show the one-line failure, sets it, and shows repr(settings) masking a token field.
Hands-on
Write s1/orders/app/settings.py: @dataclass(frozen=True) Settings(input_path: Path, out_dir: Path, log_level: str = "INFO", api_token: str | None = None) with from_env(env: Mapping[str, str]) raising ConfigError("ORDERS_INPUT is required") when missing, and __repr__ masking api_token. s1/settings_demo.py shows both cases.
Explain
Why pass Settings down rather than reading os.environ where needed?
Check
Settings fails fast and masks the token — checked (T3):
seat_fileseat_run1.4Structured logging as an adapter8 minchecked · T3
Predict
Why one JSON object per log line rather than prose?
Lesson
A logging.Formatter subclass renders each record as one JSON object: ts, level, event, and any extra fields passed as logger.info("order.rejected", extra={...}). Configure it once in the composition root at Settings.log_level; the domain never logs — the app and adapters do.
Demo
The tutor writes JsonFormatter in python/py300/demo/svc/adapters/logging.py, logs three events with extras, pipes stderr through python3 -m json.tool to prove each line parses, and shows a secret field being redacted by name.
Hands-on
Write s1/orders/adapters/jsonlog.py with JsonFormatter and configure(level: str) -> None; s1/log_demo.py logs "orders.read" with extra count=3 and "orders.rejected" with order_id="X1" to stderr; stdout prints only "ok". Each stderr line must be valid JSON with keys ts, level, event.
Explain
Why keep logging out of the domain layer?
Check
Log lines are JSON objects with ts, level and event — checked (T3):
seat_fileseat_run1.5The application service and its Result9 minchecked · T3
Predict
ProcessOrders.run() returns Result(read=5, posted=4, rejected=1). Who decides the exit code?
Lesson
A use case is a class with its dependencies injected (source, sink, clock) and one run() returning a frozen Result dataclass. It orchestrates domain functions, logs events and never prints, exits or reads the environment. The composition root builds it from Settings; tests build it from fakes.
Demo
The tutor writes ProcessOrders in python/py300/demo/svc/app/process.py taking OrderSource and ReportSink, returns Result, and tests it with an in-memory source and sink in five lines.
Hands-on
Write s1/orders/app/process.py: @dataclass(frozen=True) Result(read: int, posted: int, rejected: int); class ProcessOrders(source: OrderSource, sink: ReportSink) with run() -> Result that counts the orders read, posts all for now, writes "orders: N" to the sink and logs "orders.read". s1/test_process.py tests it with fakes (2 tests).
Explain
What could you test about run() without any file or database?
Check
ProcessOrders returns a Result and is tested with fakes — checked (T3):
seat_fileseat_testcraft1.6Lab: the walking skeleton10 minchecked · T3
Predict
The input file does not exist. Under the Orders contract (0 ok, 4 rejects, 8 fatal), the exit code is?
Lesson
The walking skeleton is the whole shape with almost no logic: python3 -m orders process (reading ORDERS_INPUT and ORDERS_OUT from the environment, or --input/--out), JSON logs to stderr, "orders: N" to stdout and to <out>/report.txt, exit 0; an unreadable file logs "orders.fatal" and exits 8. Every layer is touched once.
Demo
The tutor wires python/py300/demo/svc/cli/main.py and __main__.py, runs it with the sample, with a missing file, and with the variable unset, reading the exit code each time with echo $?.
Hands-on
Complete s1/orders/cli/main.py and s1/orders/__main__.py: build Settings (env, with --input/--out overrides), configure logging, wire JsonFileSource and TextFileSink into ProcessOrders, map Result to exit 0, FileNotFoundError and ConfigError to a one-line stderr message and exit 8. Run it over samples/s1/orders.json.
Explain
What does a walking skeleton prove before any business logic exists?
Check
The skeleton runs end to end with exit codes 0 and 8 — checked (T3):
seat_runseat_runseat_fileseat_lint
Session 2 · Concurrency
Six steps on doing several things at once without lying to yourself. Threads and the GIL: why threads help for waiting and not for computing. concurrent.futures: a thread pool for I/O, a process pool for CPU, futures and as_completed. asyncio: coroutines, await, gather, and what a blocking call does to the loop. Deadlines: asyncio.timeout, wait_for, and cooperative cancellation. Shared state: locks, queues, and the better answer of not sharing. Live in the seat: a CPU loop taking the same time on four threads, forty file reads finishing in the time of the slowest, a coroutine cancelled at its deadline, and a counter that loses increments until a lock arrives.
Lab Write the concurrent fetcher: read many order files at once under a total deadline, collect results and failures separately, never lose a count to a race, and prove it with tests that include a slow source. Session 2 checkpoint.
6 mini steps · ~55 min
2.1Threads and the GIL9 minchecked · T3
Predict
A pure-Python loop summing 50 million numbers is split across 4 threads. Wall time is?
Lesson
threading.Thread runs a function concurrently, but the GIL serialises Python bytecode, so threads overlap waiting (files, sockets, sleep) and not computing. Measure with time.perf_counter before deciding; join every thread you start; pass results back through a queue or a list slot, not a global.
Demo
In python/py300/demo/conc/gil_demo.py the tutor times a CPU loop single-threaded and on four threads (no gain), then times forty sleeps of 0.1 s sequentially and on threads (big gain), narrating the numbers.
Hands-on
Write s2/threads.py with timed(fn) -> float using perf_counter, cpu_work(n) summing a range, and io_work(seconds) sleeping; main prints four lines "cpu single: X", "cpu threads: X", "io single: X", "io threads: X" with 2 decimals, running 4 units each way, joining every thread.
Explain
In one sentence, when do threads help in Python?
Check
threads.py times CPU and I/O work both ways — checked (T3):
seat_fileseat_run2.2concurrent.futures9 minchecked · T3
Predict
One of eight submitted tasks raises. When does the exception surface?
Lesson
ThreadPoolExecutor and ProcessPoolExecutor share one API: submit returns a Future, map keeps order, as_completed yields in finish order, result() re-raises the task's exception. Use with so the pool shuts down; size the thread pool for I/O, the process pool for CPU cores.
Demo
The tutor reads forty sample files with a ThreadPoolExecutor in python/py300/demo/conc/pool_demo.py, collects results with as_completed, injects one missing file, and shows the exception caught per future while the rest succeed.
Hands-on
Write s2/pool.py: read_all(paths: list[Path], workers: int = 8) -> tuple[dict[Path, int], dict[Path, str]] returning line counts and error messages per path, using ThreadPoolExecutor and as_completed. Main runs it over samples/s2/orders/*.json plus one missing path and prints "ok: N" and "failed: 1".
Explain
Why collect failures per future instead of letting the first one stop everything?
Check
pool.py reads files concurrently and separates failures — checked (T3):
seat_fileseat_run2.3asyncio: coroutines and gather9 minchecked · T3
Predict
Inside a coroutine you call time.sleep(1) instead of await asyncio.sleep(1). What happens to the other tasks?
Lesson
async def makes a coroutine; await yields to the event loop while waiting; asyncio.gather runs many at once and returns their results in order (or raises the first exception unless return_exceptions=True); asyncio.run starts the loop. Never block inside a coroutine; wrap blocking calls with asyncio.to_thread.
Demo
In python/py300/demo/conc/async_demo.py the tutor gathers ten fake fetches with asyncio.sleep, shows total time equal to the slowest, replaces one sleep with time.sleep to show the stall, then fixes it with to_thread.
Hands-on
Write s2/aio.py: async fetch(name: str, delay: float) -> str returning name after asyncio.sleep(delay); async main() gathering five fetches with delays 0.1–0.5 and printing the names joined by "," plus "elapsed: X" with one decimal; must finish in well under 1.5 seconds (parallel, not sequential).
Explain
Why did the gather finish in the time of the slowest fetch, not the sum?
Check
aio.py gathers five coroutines concurrently — checked (T3):
seat_fileseat_run2.4Deadlines and cancellation9 minchecked · T3
Predict
async with asyncio.timeout(0.2): await slow(). slow() takes 1 s. What is raised, and does slow() keep running?
Lesson
Every wait needs a deadline. asyncio.timeout(seconds) and wait_for cancel the awaited work and raise TimeoutError; cancellation is cooperative — it arrives at an await as CancelledError, which you may clean up after but must re-raise. For threads, use a shared Event and check it in the loop.
Demo
The tutor wraps a slow coroutine with asyncio.timeout in python/py300/demo/conc/deadline_demo.py, logs the CancelledError inside it with try/finally, then shows a thread loop stopping on an Event within one iteration.
Hands-on
Write s2/deadline.py: async slow(seconds) that prints "cancelled" in a finally-style except CancelledError and re-raises; async main() running slow(2) under asyncio.timeout(0.3) and printing "timed out" when TimeoutError is caught; then a threaded worker(stop: Event) stopped after 0.2 s printing "stopped".
Explain
Why must you re-raise CancelledError after cleaning up?
Check
deadline.py times out, cancels cooperatively and stops a thread — checked (T3):
seat_fileseat_run2.5Shared state: locks, queues, or neither9 minchecked · T3
Predict
Four threads each do counter += 1 ten thousand times. The final counter is?
Lesson
Shared mutable state needs a plan: a threading.Lock around the critical section, a queue.Queue to hand work and results between threads, or — best — no sharing: each worker returns its own result and the main thread combines them. Prefer the last; it is also the easiest to test.
Demo
In python/py300/demo/conc/race_demo.py the tutor runs the unlocked counter and gets 39,217, adds a Lock and gets 40,000, then rewrites it so each thread returns its count and the main thread sums — no lock needed.
Hands-on
Write s2/shared.py with three versions of counting to 40000 on 4 threads: racy (no lock), locked (threading.Lock), and shared_nothing (each thread returns its count via a list slot or Queue). Print "racy: N", "locked: 40000", "shared_nothing: 40000".
Explain
Why is shared-nothing preferable to a lock when you can arrange it?
Check
shared.py shows the race and two fixes — checked (T3):
seat_fileseat_run2.6Lab: the concurrent fetcher10 minchecked · T3
Predict
Twelve sources, one takes 5 s, the deadline is 1 s. fetch_all returns?
Lesson
fetch_all(sources: list[OrderSource], deadline: float) -> FetchResult runs every source concurrently with asyncio (adapters wrapped in to_thread), applies the deadline per source, and returns frozen FetchResult(orders: list[dict], failures: dict[str, str]). No shared mutable state; tests use a fake slow source.
Demo
The tutor builds fetch_all in python/py300/demo/conc/fetch_demo.py over the sample folder plus a SlowSource fake, runs it with deadline 0.5, shows one failure and the rest succeeding, and writes the test with the fake.
Hands-on
Write s2/orders/app/fetch.py with FetchResult and async fetch_all(sources, deadline) as specified, plus s2/test_fetch.py with tests: all fast sources succeed; one slow fake is recorded as "timeout" and the others succeed; a source raising FileNotFoundError is recorded with its message; total time under deadline + 0.5 s.
Explain
Which design choice makes fetch_all testable without real files or real time?
Check
fetch_all runs sources concurrently under per-source deadlines, with four tests — checked (T3):
seat_fileseat_testseat_lint
Session 3 · Persistence
Six steps on storage that the domain never sees. The repository Protocol: save, get, list_pending — in domain terms, no SQL. The sqlite3 adapter: a schema, hand-written migrations tracked in a schema_version table, and why a migration is a one-way script. Transactions: with conn as a unit of work, rollback on error, and what isolation means for a batch. Mapping rows to records and back without an ORM. A fake repository for tests, and contract tests that run the same assertions against the fake and the real one. Live in the seat: a repository swapped under a use case, a migration applied twice doing nothing the second time, a rollback leaving zero rows, and the same test file passing against both implementations.
Lab Write OrdersRepository as a Protocol with a sqlite3 adapter (migrations, transactions) and an in-memory fake, with one contract test suite that passes against both and hidden contract tests from the grader. Session 3 checkpoint.
6 mini steps · ~53 min
3.1The repository Protocol8 minchecked · T3
Predict
Which method belongs on OrdersRepository: list_pending() -> list[Order], or execute_sql(query)?
Lesson
A repository offers the domain's questions and answers — save(order), get(order_id) -> Order | None, list_pending() -> list[Order], mark_posted(order_id) — and nothing about tables. The use case depends on the Protocol; a fake implements it with a dict; the sqlite adapter implements it with SQL.
Demo
The tutor writes OrdersRepository in python/py300/demo/persist/app/ports.py and an InMemoryOrders fake, then rewires ProcessOrders to it, showing the use case unchanged.
Hands-on
Write s3/orders/domain/order.py (frozen Order(order_id, customer, total: Decimal, status: Status enum)) and s3/orders/app/ports.py with OrdersRepository(Protocol) having save, get, list_pending, mark_posted. s3/orders/adapters/memory.py implements it over a dict. s3/repo_demo.py saves two, marks one posted, prints "pending: 1".
Explain
What would break if the Protocol exposed SQL?
Check
The repository Protocol and in-memory fake exist — checked (T3):
seat_fileseat_run3.2Schema and migrations by hand9 minchecked · T3
Predict
migrate() is run twice against the same database. What should happen the second time?
Lesson
Keep a list of numbered migration scripts (SQL strings) and a schema_version table. migrate(conn) reads the current version, applies each later script inside a transaction and records it. Never edit an applied migration; add a new one. This is how schemas evolve safely without a framework.
Demo
In python/py300/demo/persist/adapters/migrations.py the tutor writes two migrations (create orders; add column posted_at), runs migrate twice, inspects schema_version, then adds a third migration and runs again to show only it applies.
Hands-on
Write s3/orders/adapters/migrations.py: MIGRATIONS = [(1, "CREATE TABLE orders(...)"), (2, "ALTER TABLE orders ADD COLUMN posted_at TEXT")] and migrate(conn) -> int returning the version reached, idempotent. s3/migrate_demo.py runs it twice on :memory: printing "version: 2" both times and the orders column names.
Explain
Why never edit a migration that has run somewhere?
Check
migrate() is idempotent and tracks a version — checked (T3):
seat_fileseat_run3.3Transactions and the unit of work9 minchecked · T3
Predict
with conn: insert A; insert B (fails). After the block, how many rows exist?
Lesson
A batch of writes is one unit of work: either all land or none. with conn: does that for sqlite3; inside, raise to roll back. Keep transactions short, never await or sleep inside one, and let the repository method own its transaction unless the use case passes one in deliberately.
Demo
The tutor inserts two orders where the second violates a UNIQUE constraint in python/py300/demo/persist/tx_demo.py, shows zero rows after the rollback, then shows the same without the with block leaving one row.
Hands-on
Write s3/orders/adapters/sqlite_repo.py: SqliteOrders(conn) implementing OrdersRepository with parameterised SQL and save_all(orders) in one transaction that rolls back entirely if any order is a duplicate id. s3/tx_demo.py proves it: a batch with a duplicate leaves "rows: 0", a clean batch "rows: 3".
Explain
Why keep transactions short?
Check
SqliteOrders rolls back a failed batch — checked (T3):
seat_fileseat_runcraft3.4Mapping rows to records8 minchecked · T3
Predict
total is stored as INTEGER cents. The row comes back as 12345. Order.total should be?
Lesson
Two small functions per table: to_row(order) -> tuple and from_row(row) -> Order, with every conversion in them: Decimal to cents, Status to text, datetime to ISO. Use sqlite3.Row for named access. Keep the mapping next to the SQL and test it in isolation with one round trip.
Demo
The tutor adds to_row/from_row to the demo adapter, sets conn.row_factory = sqlite3.Row, round-trips an order and asserts equality, then changes a column type to show the mapping catching it.
Hands-on
Add to_row and from_row to s3/orders/adapters/sqlite_repo.py (cents, status text, ISO timestamps) and s3/test_mapping.py with tests: round trip equality; a total of Decimal("0.10") survives; status comes back as the enum, not text.
Explain
Why keep the mapping functions separate from the SQL execution?
Check
Row mapping round-trips with three tests — checked (T3):
seat_fileseat_testcraft3.5Fakes and contract tests9 minchecked · T3
Predict
A test suite is parametrized over [InMemoryOrders, SqliteOrders]. What does it prove?
Lesson
A fake is a real implementation with a simpler store; a mock records calls. Prefer fakes for repositories. Contract tests: one test module whose fixture is parametrized over every implementation, asserting the Protocol's promises (save then get, list_pending excludes posted, mark_posted of an unknown id raises).
Demo
The tutor writes python/py300/demo/persist/test_contract.py with a repo fixture parametrized by implementation, runs it green on both, breaks the fake's list_pending and shows only the fake's row fail.
Hands-on
Write s3/test_contract.py: a fixture repo parametrized with params=["memory", "sqlite"] building each implementation (sqlite on :memory: after migrate), and at least five contract tests (save/get, get missing -> None, list_pending excludes posted, mark_posted unknown raises KeyError, save_all is atomic).
Explain
Why run the same tests against the fake?
Check
Contract tests pass against the fake and sqlite — checked (T3):
seat_fileseat_test3.6Lab: the repository, proven on both sides10 minproven · T4
Predict
The grader adds a hidden contract test: saving an order twice updates rather than duplicates. Which implementation is most likely to fail it?
Lesson
The session lab finishes the repository: migrations, SqliteOrders with upsert semantics and atomic save_all, InMemoryOrders, mapping, and the contract suite — then the grader runs hidden contract tests against both implementations. The fake and the real thing must agree on everything the Protocol promises.
Demo
The tutor runs the full contract suite in the demo, then a hidden-style test (save twice, list_pending order is by order_id), and fixes the sqlite adapter with INSERT ... ON CONFLICT DO UPDATE.
Hands-on
Make s3/orders/adapters/{memory,sqlite_repo,migrations}.py complete: save upserts, list_pending returns orders sorted by order_id, save_all is atomic, migrate is idempotent. All contract tests green; basedpyright clean. Then ask the tutor to check: hidden contract tests run against both.
Explain
Which promise of the Protocol is easiest to get wrong in SQL but trivial in the fake?
Check
Both repositories pass the contract suite and the hidden contract tests — proven (T4):
seat_fileseat_testseat_lintcraft
Session 4 · An HTTP API on the standard library
Six steps that put Orders on the network with nothing but http.server and urllib, so every mechanism is visible. The anatomy of a request and a response. A small router mapping method and path pattern to a handler. JSON in and out, status codes that mean what they say, and validation at the edge returning 400 with a reason. A client with urllib.request that handles HTTPError and timeouts. Testing the server in-process: start it on port 0 in a thread, call it, stop it. Live in the seat: a raw request typed by hand, a 404 and a 405 told apart, a 400 for a bad body, a client timing out at 0.5 s, and a server test that runs in under a second.
Lab Write the Orders API: POST /orders validates and prices an order through the application service and returns 201 with the result, GET /orders/{id} returns 200 or 404, bad input gives 400 — all tested in-process. Session 4 checkpoint.
6 mini steps · ~53 min
4.1Request, response, and http.server8 minchecked · T3
Predict
A handler writes the body but never calls send_response(). The client sees?
Lesson
An HTTP exchange is text: a request line, headers, a blank line, a body; a status line, headers, a blank line, a body back. BaseHTTPRequestHandler gives do_GET/do_POST with self.path, self.headers and self.rfile; you call send_response, send_header, end_headers, then write the body bytes. ThreadingHTTPServer serves concurrently.
Demo
The tutor writes a two-line GET /health handler in python/py300/demo/http/hello_server.py, starts it on 8765, calls it with urllib and shows the raw response text, then omits end_headers to show the broken reply.
Hands-on
Write s4/health_server.py: a ThreadingHTTPServer with GET /health returning 200 and the JSON body {"status": "ok"} with Content-Type application/json, and 404 with {"error": "not found"} for any other path. With the argument --selftest, main starts it on port 0 in a thread, requests /health and /nope with urllib, prints "selftest ok" if the statuses are 200 and 404, and shuts the server down.
Explain
Why must headers be sent before the body?
Check
health_server.py answers /health with JSON and 404 elsewhere — checked (T3):
seat_fileseat_run4.2A small router9 minchecked · T3
Predict
GET /orders/42 matches the pattern /orders/{id}. The router should pass the handler?
Lesson
A router is a list of (method, compiled pattern, handler). Patterns like /orders/{id} compile to regexes with named groups. match(method, path) returns the handler and params, or tells you 404 (no path matched) versus 405 (path matched, wrong method). Keep it in its own module and test it without a socket.
Demo
In python/py300/demo/http/router.py the tutor writes Router.add and Router.match, tests five cases including 404 and 405 in a pytest file, and plugs it into the handler's do_GET/do_POST.
Hands-on
Write s4/orders/adapters/http/router.py: Router with add(method, pattern, handler) and match(method, path) -> Match | NotFound | MethodNotAllowed, patterns with {name} segments; s4/test_router.py with at least five tests: exact match, param extraction, two params, 404, 405.
Explain
Why distinguish 404 from 405?
Check
The router matches patterns and tells 404 from 405, with five tests — checked (T3):
seat_fileseat_test4.3JSON bodies, status codes and validation at the edge9 minchecked · T3
Predict
POST /orders with a body of 50 MB of valid JSON. What should happen first?
Lesson
Read exactly Content-Length bytes (cap it: 413 above a limit), parse JSON (400 on failure), validate shape and ranges with a pure function returning a list of problems (400 with {"errors": [...]}), then call the use case. 201 for created with a Location header, 200 for reads, 404 for unknown ids, 500 only for your own bugs — logged, never with a traceback in the body.
Demo
The tutor builds read_json(handler, limit) and a validate_order(dict) -> list[str] in python/py300/demo/http/edge.py, sends a too-large body, a non-JSON body and a body with qty -1, and reads the three responses.
Hands-on
Write s4/orders/adapters/http/edge.py: read_json(handler, limit=65536) raising TooLarge/BadJson; validate_order(body: dict) -> list[str] checking customer (non-empty str), lines (non-empty list of {sku: str, qty: int > 0, unit_price: decimal string}); s4/test_edge.py with at least six tests over validate_order and the two read errors.
Explain
Why return a list of problems rather than raise on the first?
Check
Edge validation returns all problems; size and JSON errors are typed — checked (T3):
seat_fileseat_test4.4The client: urllib, errors and timeouts8 minchecked · T3
Predict
urllib.request.urlopen(req) and the server answers 404. What happens?
Lesson
Build a Request with method, headers and a bytes body; call urlopen with a timeout always; HTTPError for status errors (read e.code), URLError for connection failures, TimeoutError (socket.timeout) for deadlines. Wrap it in a small client class with typed methods so the rest of the code never sees urllib.
Demo
The tutor writes OrdersClient in python/py300/demo/http/client.py against the hello server, shows a 404 becoming a typed NotFound, and a 0.2 s timeout against a handler that sleeps.
Hands-on
Write s4/orders/adapters/http/client.py: OrdersClient(base_url, timeout=2.0) with get_order(order_id) -> dict (raises NotFound on 404) and create_order(body) -> dict (raises Invalid(errors) on 400), both using urlopen with the timeout and catching HTTPError/URLError into ClientError types. s4/test_client.py tests the mapping against the 4.1 server (3 tests).
Explain
Why must every urlopen have a timeout?
Check
OrdersClient maps HTTP errors to typed errors with timeouts — checked (T3):
seat_fileseat_test4.5Testing a server in-process9 minchecked · T3
Predict
Why start the test server on port 0?
Lesson
A fixture builds the server with fake dependencies on ("127.0.0.1", 0), starts serve_forever in a daemon thread, yields the base URL, and calls shutdown() and server_close() after. Tests then use OrdersClient or urllib directly. The whole suite runs in well under a second with no sockets left open.
Demo
The tutor writes the server fixture in python/py300/demo/http/conftest.py, two tests against /health, and shows pytest --durations to prove each test takes milliseconds.
Hands-on
Write s4/conftest.py with a fixture server_url that starts the 4.1 health server on port 0 in a thread and stops it; s4/test_health.py with tests for 200 JSON on /health, 404 on /nope, and Content-Type application/json — using OrdersClient or urllib.
Explain
What two things must the fixture do after yield?
Check
An in-process server fixture drives three passing tests — checked (T3):
seat_fileseat_test4.6Lab: the Orders API10 minchecked · T3
Predict
POST /orders succeeds. The response should be?
Lesson
The API adapter wires router, edge validation and the application service (PriceOrder use case over the repository from session 3): POST /orders → 400 with errors, or 201 with {"order_id", "total"} and Location; GET /orders/{id} → 200 or 404; anything else 404/405. The domain and app layers do not know HTTP exists.
Demo
The tutor assembles python/py300/demo/http/api.py from the pieces, runs the in-process tests for the four outcomes, and traces one POST through router → edge → use case → repository → response.
Hands-on
Write s4/orders/adapters/http/api.py: make_server(service, repo, port=0) returning a ThreadingHTTPServer with the routes above; s4/test_api.py with at least six in-process tests: 201 + Location, body echoes total "12.50" for a known fixture, 400 lists two errors for a bad body, 404 unknown id, 405 wrong method, 413 oversize.
Explain
Which layer would change if you replaced http.server with a framework tomorrow?
Check
The Orders API passes six in-process tests — checked (T3):
seat_fileseat_testseat_lintcraft
Session 5 · Testing at scale
Six steps on a test suite you can trust at three in the morning. The pyramid: many fast unit tests over the domain, fewer integration tests over sqlite and HTTP, a handful end to end. Fakes over mocks, and the one place a mock is right. Integration tests that migrate a fresh database per test. Property-style tests: generate many cases from a seed and assert invariants, not examples. Golden tests: compare a report to an approved file and review the diff when it changes. A coverage floor with trace, and how to kill a flaky test. Live in the seat: a suite timed by layer, a thousand random orders priced without a negative total, a golden file diff, and a test that fails one run in five until the clock is injected.
Lab Assemble the Orders suite: unit, integration, property-style and golden tests with a coverage floor and no flakiness over 20 runs; the grader adds hidden property and golden cases. Session 5 checkpoint.
6 mini steps · ~52 min
5.1The pyramid, fakes and the one good mock8 minchecked · T3
Predict
You want to assert that the use case logged "orders.rejected" exactly once. Fake or mock?
Lesson
Shape the suite: domain tests (pure, thousands per second), app tests with fakes, adapter tests against the real sqlite and in-process HTTP, one or two end-to-end runs of the CLI. Fakes hold state and behave; mocks verify calls — reserve them for third-party boundaries you cannot fake, such as a payment provider.
Demo
The tutor labels every test in python/py300/demo/tests/ with a pytest marker (unit, integration, e2e), runs pytest -m unit --durations=5, then shows a RecordingLog fake replacing a mock assertion.
Hands-on
Add markers unit/integration/e2e to s5/pytest.ini and mark every test in s1–s4 copies under s5/tests/. Write s5/tests/fakes.py with RecordingLog and a test proving ProcessOrders logs "orders.read" once. pytest -m unit must run in under 2 seconds.
Explain
When is a mock the right tool?
Check
Markers configured and a RecordingLog fake in use — checked (T3):
seat_fileseat_fileseat_test5.2Integration tests over a fresh database8 minchecked · T3
Predict
Two integration tests share one sqlite file and one inserts order A. The second test asserts an empty repository. Result?
Lesson
An integration test uses the real adapter against a fresh store: a fixture creates a sqlite file in tmp_path (or :memory:), runs migrate, yields the repository, closes the connection. Tests are independent, can run in any order, and prove the SQL, the mapping and the migrations together.
Demo
The tutor writes the fresh_repo fixture in python/py300/demo/tests/conftest.py, runs the integration tests with -p no:randomly … then shuffles them by hand to show independence, and times them.
Hands-on
Write s5/tests/conftest.py with fresh_repo (tmp_path sqlite file, migrated, closed after) and s5/tests/test_repo_integration.py with at least four integration tests: save/get survives a reopen of the file, list_pending after mark_posted, save_all atomic on duplicate, migrate on an existing file is a no-op.
Explain
Why a file in tmp_path rather than :memory: for one of the tests?
Check
Integration tests run on a fresh migrated database each — checked (T3):
seat_fileseat_test5.3Property-style tests9 minchecked · T3
Predict
A property test generates 500 random orders with random.Random(42). Why the fixed seed?
Lesson
Without a property-testing library you can still test properties: a generator of random valid orders from random.Random(seed), a loop of a few hundred cases, and invariants — total >= 0, total == sum of lines minus discount plus shipping, pricing is deterministic, rounding never loses a cent across lines. On failure, print the case.
Demo
The tutor writes gen_order(rng) and three invariant tests in python/py300/demo/tests/test_props.py, plants an off-by-a-cent rounding bug in pricing, and watches the property test find a case the examples missed.
Hands-on
Write s5/tests/gen.py with gen_order(rng: random.Random) -> dict producing valid orders (1–6 lines, qty 1–20, prices 0.01–500.00 as strings), and s5/tests/test_props.py with at least three invariant tests over 300 cases each (non-negative total, deterministic pricing, total equals the hand-computed sum of quantized lines ± rules).
Explain
What do properties catch that examples do not?
Check
Three property-style tests over seeded random orders pass — checked (T3):
seat_fileseat_fileseat_test5.4Golden tests8 minchecked · T3
Predict
A golden test fails after you changed the report's date format on purpose. What do you do?
Lesson
A golden (approval) test renders a report from a fixed input and compares it byte for byte with an approved file in tests/golden/. On mismatch it shows a unified diff. Re-approve by copying the new output over the golden file only after reading the diff. Inject the clock so timestamps are fixed.
Demo
The tutor writes assert_golden(name, text) in python/py300/demo/tests/golden.py using difflib, approves the sample report, changes a column width and reads the diff.
Hands-on
Write s5/tests/golden.py with assert_golden(name: str, actual: str) comparing to s5/tests/golden/<name>.txt and failing with a unified diff; s5/tests/test_golden.py rendering the Orders report for samples/s5/orders.json with a fixed clock and comparing to golden/report.txt, which you approve on the first run.
Explain
Why inject the clock into the report?
Check
A golden test compares the report to an approved file with a diff on failure — checked (T3):
seat_fileseat_fileseat_test5.5Coverage floors and flaky tests9 minchecked · T3
Predict
A test passes 19 times and fails once with a timing assertion. The right fix is?
Lesson
A coverage floor (trace --count --missing over the domain and app packages) stops testing from eroding: record the lines covered and fail the lab if a module drops. Flaky tests come from time, order, shared state and randomness without a seed; fix the cause — inject the clock, wait on events, isolate state, seed the generator.
Demo
The tutor runs the trace module over the demo domain and app, writes COVERAGE.md, then runs a flaky sleep-based test 20 times in a shell loop, finds the failure, and replaces the sleep with an Event wait.
Hands-on
Write s5/run_coverage.sh that runs the trace module over s5/tests and writes s5/COVERAGE.md with per-module missed lines for orders/domain and orders/app (must be 0 missed); find and fix the flaky test in samples/s5/test_flaky.py (copy to s5/tests/test_fixed.py) so that 20 consecutive runs pass.
Explain
Name the four usual causes of flaky tests.
Check
Coverage floor recorded at zero missed; the flaky test is fixed — checked (T3):
seat_fileseat_fileseat_test5.6Lab: the suite, with hidden properties and goldens10 minproven · T4
Predict
The grader runs your pricing against 1,000 hidden random orders and one hidden golden file. What makes you confident?
Lesson
The lab is the complete suite over sessions 1–4 code: markers, fakes, integration over fresh databases, property tests with seeds, golden report, coverage floor at zero missed, no flaky test in 20 runs. The grader runs hidden property cases through your pricing and compares a hidden golden report.
Demo
The tutor runs the full demo suite with --durations=10, shows the pyramid shape in numbers, then runs a hidden-style property case that exposes a rounding bug in a deliberately broken pricing module.
Hands-on
Make s5/tests/ complete and green: at least 40 tests across unit, integration and e2e markers, properties over 300 seeds, the golden report approved, COVERAGE.md at 0 missed for domain and app. Then ask the tutor to check: hidden property and golden cases run.
Explain
Which kind of test in your suite would you keep if you could keep only one, and why?
Check
The suite is green with 40+ tests and passes hidden property and golden cases — proven (T4):
seat_testseat_filecraft
Session 6 · Performance
Six steps on making it fast without guessing. Measure first: timeit for expressions, perf_counter for sections, and a benchmark script you can rerun. cProfile and pstats to find where the time really goes, and the surprise it usually brings. Complexity in practice: the list that should have been a set, the nested loop that should have been a dict lookup. Memory: tracemalloc, generators, __slots__. I/O batching: executemany, buffered writes, fewer round trips. Then the discipline: change one thing, measure again, keep the proof. Live in the seat: a profile that blames string formatting, a 200× speed-up from one set(), a tracemalloc snapshot halved by a generator, and a before/after table in a markdown file.
Lab Speed up the supplied slow pricing pipeline by at least 10× with identical output, and record each change with its measurement in a before/after table. Session 6 checkpoint.
6 mini steps · ~52 min
6.1Measure first8 minchecked · T3
Predict
You time a function once and get 0.31 s. How reliable is that number?
Lesson
timeit.repeat for small expressions (take the min), time.perf_counter around a section, and a bench.py that runs the real workload N times on fixed data and prints a median. Measure on the same data every time, write the number down with the commit, and only then change code.
Demo
The tutor writes python/py300/demo/perf/bench.py around the slow pipeline over samples/s6/orders_10k.json, runs it three times, and shows the spread before fixing anything.
Hands-on
Generate the data once with python3 samples/s6/make_orders.py s6/orders_10k.json (deterministic, 10,000 orders). Write s6/bench.py: load it once, run price_all(orders) from s6/pricing.py (a copy of samples/s6/slow_pricing.py) 5 times, print "median: X ms" and "min: X ms" with perf_counter, and write the baseline line to s6/PERF.md as "baseline: <median> ms".
Explain
Why the median or minimum rather than the mean?
Check
bench.py measures the baseline and records it — checked (T3):
seat_fileseat_runseat_file6.2cProfile: where the time goes9 minchecked · T3
Predict
The profile shows 70% of time in str.format called from format_line. The best next step is?
Lesson
python3 -m cProfile -o out.prof s6/bench.py then pstats: sort by cumulative and tottime, read the top ten, follow callers. The hot spot is rarely where you guessed. Fix one, re-run the benchmark, re-profile; stop when the profile is flat or fast enough.
Demo
The tutor profiles the slow pipeline, sorts by tottime, finds a lookup function called 10,000 × 300 times, and shows the call graph with print_callers.
Hands-on
Profile s6/bench.py with cProfile into s6/baseline.prof; write s6/profile_top.py that loads it with pstats and prints the top 5 functions by tottime; copy the top 5 lines into s6/PERF.md under a heading "Profile (baseline)" and name the hot spot in one sentence.
Explain
What is the difference between tottime and cumtime?
Check
A baseline profile exists and PERF.md names the hot spot — checked (T3):
seat_fileseat_fileseat_file6.3Complexity in practice9 minchecked · T3
Predict
for order in orders: if order.sku in sku_list — with 10,000 orders and 300 skus. Changing sku_list to a set changes the work from?
Lesson
Know the cost of the operations you reach for: list membership and index() are O(n), dict and set membership O(1), sorting O(n log n), string concatenation in a loop O(n²) versus join. A nested loop over two collections is the first thing to look for; build an index once and look up.
Demo
In python/py300/demo/perf/complexity_demo.py the tutor times in on a list versus a set at 300, 3,000 and 30,000 elements, then rewrites the slow pipeline's catalogue lookup to a dict and re-runs the bench: 40× faster.
Hands-on
Fix the catalogue lookup in s6/pricing.py (build a dict sku -> price once), re-run s6/bench.py, and append "after dict lookup: <median> ms" to s6/PERF.md. Output of price_all must be unchanged: s6/test_pricing_same.py runs both samples/s6/slow_pricing.price_all and s6/pricing.price_all over the first 500 orders and asserts equality.
Explain
Why build the index once outside the loop?
Check
The lookup is a dict and the output is unchanged — checked (T3):
seat_fileseat_testseat_file6.4Memory: tracemalloc, generators, __slots__8 minchecked · T3
Predict
Loading 10,000 orders into a list of dicts then a list of dataclasses then a list of tuples. Which uses least memory?
Lesson
tracemalloc.start(), take_snapshot() and compare_to show where memory went. Generators avoid materialising lists you only walk once; @dataclass(slots=True) removes the per-instance dict. Memory and time often trade; measure both.
Demo
The tutor snapshots the slow pipeline's peak with tracemalloc in python/py300/demo/perf/mem_demo.py, converts the intermediate list to a generator and the record to slots=True, and shows the peak halving.
Hands-on
Write s6/mem.py that reports peak memory (tracemalloc.get_traced_memory) for price_all over the 10k file as "peak: X MB" with one decimal; then make the pipeline's intermediate list a generator and the line record slots=True; append "peak before: X MB" and "peak after: X MB" to PERF.md (after must be lower).
Explain
When does a generator not save memory?
Check
Peak memory measured before and after — checked (T3):
seat_fileseat_runseat_fileseat_file6.5I/O batching8 minchecked · T3
Predict
10,000 inserts each in its own transaction versus one transaction with executemany. Expected speed-up on sqlite?
Lesson
Round trips dominate I/O: one transaction for a batch, executemany instead of a loop of execute, writes gathered into a list and joined once, reads in pages. The same rule applies to HTTP calls — batch endpoints, connection reuse — and to files — read once, write once.
Demo
The tutor times 10,000 single-transaction inserts against one executemany in python/py300/demo/perf/batch_demo.py (6 s versus 0.05 s), then shows the report writer joining lines once instead of writing per line.
Hands-on
Write s6/batch.py timing save of the 10k priced orders into a tmp sqlite file two ways: per-row commits and one executemany transaction; print "per-row: X ms" and "batched: X ms" and append both to PERF.md. Then make the repository's save_all use executemany.
Explain
What does a commit actually cost?
Check
Batched writes measured and recorded — checked (T3):
seat_fileseat_runseat_file6.6Lab: ten times faster, same answer10 minchecked · T3
Predict
Your final pipeline is 14× faster but one total differs by a cent. Does the lab pass?
Lesson
The lab closes the loop: the pipeline from samples/s6/slow_pricing.py at least 10× faster by the bench, byte-identical output against expected_10k.json, every change a line in PERF.md with its measurement, and the profile after showing a flat top. Optimisation is a series of measured, reversible steps.
Demo
The tutor shows the demo PERF.md as a before/after table with five rows and the final profile, then re-runs the equality test as the last word.
Hands-on
Finish s6/pricing.py so that s6/bench.py --compare (which times samples/s6/slow_pricing.price_all and s6/pricing.price_all back to back and prints "speedup: Nx") reports at least 10x, s6/test_pricing_same.py passes, s6/PERF.md has a table with at least four measured changes and a final "after: <median> ms" line, and s6/after.prof exists.
Explain
Which single change gave the biggest gain, and what did the profile say before it?
Check
Pipeline ≥10× faster with identical output and a recorded proof — checked (T3):
seat_runseat_testseat_fileseat_file
Session 7 · Reading, reviewing and the AI era
Six steps on code you did not write. Reading an unfamiliar module: entry point first, then the data, then the invariants, before any opinion. A review checklist that finds defects rather than style. Writing a review that a stranger can act on: location, evidence, risk, suggestion. Security basics for a batch service: untrusted input, path handling, secrets, injection. Verifying machine-generated code: treat it as a pull request from someone you have never met, and write the test that would catch its lie. Live in the seat: review_me.py read in ten minutes, five planted defects found one by one with a reproducing test each, a path traversal in a filename, and a generated function that passes its own examples and fails the property.
Lab Review the supplied review_me.py: find all five planted defects, write a reproducing test for each, and write REVIEW.md in the house format. Session 7 checkpoint.
6 mini steps · ~54 min
7.1Reading a module you did not write9 minchecked · T3
Predict
You open an unfamiliar 400-line module. Where do you start?
Lesson
Reading order: the public entry point, the main data shapes, one path of data end to end, the error paths, then the invariants you believe hold (write them down). Only then form opinions. Ten minutes of reading saves an hour of wrong review.
Demo
The tutor reads python/py300/samples/s7/review_me.py aloud in that order, drawing the data flow in demo/READING.md: entry, shapes, one record's path, the three invariants they believe hold.
Hands-on
Read samples/s7/review_me.py (an order batch pricer). Write s7/READING.md with: the entry point, the data shapes (as you understand them), one record's path in five or fewer steps, and at least three invariants the code appears to rely on. No defects yet — understanding only.
Explain
Why write down invariants before looking for bugs?
Check
READING.md records entry point, shapes, a data path and invariants — checked (T3):
seat_file7.2A defect-finding checklist9 minchecked · T3
Predict
Which comment belongs in a defect review: "rename x to total" or "line 48 divides by len(lines) which can be 0"?
Lesson
The checklist: boundaries (empty, one, max, negative), types (text versus number, naive versus aware), money (float, rounding), errors (swallowed, wrong class), resources (unclosed), concurrency (shared state), security (paths, input size, injection, secrets), contracts (exit codes, return shapes). Ask each question of each function.
Demo
The tutor runs the checklist over the first two functions of review_me.py, finds the first planted defect (an empty batch dividing by zero), and writes a failing test for it in demo/review/test_defects.py.
Hands-on
Apply the checklist to samples/s7/review_me.py and record findings in s7/FINDINGS.md: one line per candidate with function, line, checklist item and the input that would expose it. Then write s7/test_defects.py with a failing test for at least two of them.
Explain
Which checklist item found your first defect?
Check
FINDINGS.md lists candidates and two tests reproduce defects — checked (T3):
seat_fileseat_test7.3Writing the review8 minchecked · T3
Predict
A review entry says "this is wrong". What is missing?
Lesson
House format per finding: Where (file:line, function), What (one sentence), Evidence (the input and what happens; the test name), Risk (data loss, wrong money, crash, security), Suggestion (how to fix, in a sentence). Order by risk. End with what is good about the code — reviews are read by people.
Demo
The tutor turns two findings into REVIEW.md entries in the house format, reorders by risk, and adds the closing paragraph.
Hands-on
Write s7/REVIEW.md in the house format for the defects you have confirmed so far (at least two), each with Where, What, Evidence, Risk, Suggestion, ordered by risk, and a closing paragraph naming two things the module does well.
Explain
Why order findings by risk rather than by line number?
Check
REVIEW.md follows the house format — checked (T3):
seat_file7.4Security basics for a batch service9 minchecked · T3
Predict
The report is written to out_dir / order["customer"] + ".txt". A customer named "../../etc/passwd" means?
Lesson
For a batch service: bound input sizes, validate every field's type and range, never build paths or SQL from untrusted text (map ids to safe names, use ? parameters), never log secrets, read secrets from the environment not files in the repo, and fail closed. Each is a checklist question and a test.
Demo
The tutor shows the traversal in review_me.py's writer with a crafted customer name, fixes it with a safe-name mapping and resolve() check, and shows a secret leaking into a log line before redacting.
Hands-on
Write s7/test_security.py with tests against samples/s7/review_me.py: a customer name containing ".." must not produce a path outside tmp_path (expect failure now); a 1 MB description must be rejected (expect failure now). Add both to FINDINGS.md and REVIEW.md with Risk: security.
Explain
Why map ids to filenames rather than sanitise the text?
Check
Two security tests expose traversal and unbounded input — checked (T3):
seat_testseat_file7.5Verifying machine-written code9 minchecked · T3
Predict
A generated function comes with three passing example tests and a confident docstring. What does that prove?
Lesson
Treat generated code as a pull request from a stranger: read it with the checklist, test the docstring's claims not the author's examples, add a property test and the boundaries, check the imports exist and the API is real, and keep what passes. The skill is verification, and it is the same skill as review.
Demo
The tutor reads samples/s7/generated_discount.py (a fluent function with two subtle bugs), writes a property test and a boundary test that expose both, and keeps the corrected version with the tests.
Hands-on
Verify samples/s7/generated_discount.py: write s7/test_generated.py with at least four tests including one property-style test over seeded random inputs and one boundary test; make them fail against the original, then fix a copy in s7/discount_fixed.py so all pass. Note each bug in s7/VERIFY.md.
Explain
What did the generated code claim that was false?
Check
Tests expose the generated code and pass against the fix — checked (T3):
seat_fileseat_testseat_file7.6Lab: the review that finds all five10 minchecked · T3
Predict
You found four defects and the key says five. What is the honest move?
Lesson
The lab closes session 7: every planted defect in review_me.py found, each with a reproducing test in s7/test_review.py that fails against the original and passes against your fixed copy s7/review_fixed.py, and REVIEW.md in the house format covering all five, ordered by risk. The grader compares your findings with the key.
Demo
The tutor shows the structure of a complete review package — tests red against the original and green against the fix, REVIEW.md, FINDINGS.md — without revealing the key.
Hands-on
Complete s7/REVIEW.md (five findings), s7/test_review.py (five tests, each named for its defect, failing against samples/s7/review_me.py and passing against s7/review_fixed.py via an import switch), and s7/review_fixed.py with the minimal fixes. Then ask the tutor to check against the key.
Explain
Which defect was hardest to find, and which checklist question finally surfaced it?
Check
All five planted defects found, reproduced and fixed — checked (T3):
seat_testseat_testseat_file
Session 8 · Capstone: Orders
Six steps that finish the Orders service from its spec and prove it. The spec into a module map and a test list. Validation with reason codes and a reject file that keeps the original record. Pricing rules in the domain with Decimal and one rounding point. The report and the reject CSV as pure renderers. The CLI with configuration, JSON logs and the exit-code contract 0 all posted, 4 some rejected, 8 fatal. Then the grader runs it over hidden files and runs hidden tests. Live in the seat: the spec on one screen and the test list on the other, a reject row carrying its reason code, a discount applied once not twice, and the five published cases passing in a row.
Lab Finish Orders: every published case in samples/s8/ matches byte for byte (report, reject file, exit code), your suite is green, and the hidden files and hidden tests pass. Course placement step.
6 mini steps · ~53 min
8.1Spec to plan8 minchecked · T3
Predict
An order has one invalid line among three. Under the spec, what happens to the order?
Lesson
Orders (samples/s8/SPEC.md): input JSON of orders with customer, lines (sku, qty, unit_price); catalogue JSON of valid skus; rules: 10% discount when the subtotal is 100.00 or more, flat 5.00 shipping under 50.00; rejects with codes E01 missing field, E02 bad qty, E03 bad price, E04 unknown sku; report and rejects.csv; exit 0/4/8.
Demo
The tutor reads the spec and writes python/py300/demo/orders_plan.md: module map over the session 1 layers, the exit-code table, and sixteen test names, marking which exist already.
Hands-on
Write s8/PLAN.md with the module map (which function in which layer), the exit-code table, the reason-code table, and at least twelve test names including the boundary cases (subtotal exactly 100.00, exactly 50.00, empty batch, all rejected, unreadable file).
Explain
Which boundary in the pricing rules is most likely to be wrong, and how will you test it?
Check
PLAN.md: module map, exit codes, reason codes, twelve tests — checked (T3):
seat_file8.2Validation and reason codes9 minchecked · T3
Predict
An order is missing customer and has a bad qty. Which reason code is recorded?
Lesson
validate(order: dict, catalogue: set[str]) -> Valid | Rejected(code, message) is a pure domain function applying the checks in spec order. Rejected keeps the whole original record so the reject file can carry it. Parametrized tests cover every code and the first-wins rule.
Demo
The tutor writes validate in python/py300/demo/orders/domain/validate.py with a parametrized test of nine cases, then shows the reject record keeping the original JSON intact.
Hands-on
Write s8/orders/domain/validate.py with Rejected(code, message, original) and Valid(order) as frozen dataclasses and validate(order, catalogue) as specified; s8/tests/test_validate.py parametrized over at least eight cases (each code, first-wins, a valid order, an empty lines list → E01).
Explain
Why keep the original record in the rejection?
Check
validate() assigns reason codes in spec order with eight tests — checked (T3):
seat_fileseat_test8.3Pricing rules with one rounding point9 minchecked · T3
Predict
Subtotal 99.995 before rounding. Is the 10% discount applied?
Lesson
price(order) -> Priced(subtotal, discount, shipping, total) in Decimal: each line qty × unit_price quantized half-up to the cent, subtotal the sum, discount 10% of subtotal (quantized) when subtotal >= 100.00, shipping 5.00 when subtotal < 50.00, total = subtotal - discount + shipping. Pure, deterministic, property-tested.
Demo
The tutor writes price() in python/py300/demo/orders/domain/pricing.py, tests the two boundaries and a rounding case, then runs the session 5 property test against it.
Hands-on
Write s8/orders/domain/pricing.py with Priced and price(order) as specified, and s8/tests/test_pricing.py: subtotal exactly 100.00 gets the discount, 99.99 does not, 49.99 pays shipping, 50.00 does not, a rounding case where line-first rounding matters, and a property test over 300 seeded orders (total == subtotal - discount + shipping, all quantized).
Explain
Why does the spec fix the rounding point?
Check
price() honours both boundaries and the rounding point, with a property test — checked (T3):
seat_fileseat_testcraft8.4The report and the reject file8 minchecked · T3
Predict
The reject CSV has columns code,message,original. How is the original record stored in one cell?
Lesson
render_report(priced: list[Priced], rejected: list[Rejected], clock) -> str: a header with the run time (injected), one line per posted order "<id> <customer> <total>", a totals block (orders posted, rejected, grand total), fixed widths. render_rejects(rejected) -> str as CSV with code,message,original (compact JSON). Both pure; both golden-tested.
Demo
The tutor writes both renderers in python/py300/demo/orders/app/render.py, approves the golden files for the sample batch, and shows the reject CSV opening cleanly with csv.DictReader.
Hands-on
Write s8/orders/app/render.py with render_report and render_rejects as specified and s8/tests/test_render.py: golden test of the report for samples/s8/orders.json with a fixed clock (approve samples/s8/expected_report.txt as given), the reject CSV parses back with DictReader and the original is valid JSON, and the empty batch renders the header and zero totals.
Explain
Why is the clock a parameter of render_report?
Check
Renderers match the published report and produce a parseable reject CSV — checked (T3):
seat_fileseat_test8.5The CLI, configuration and the exit-code contract9 minchecked · T3
Predict
The catalogue file is missing. Exit code?
Lesson
python3 -m orders process --input orders.json --catalogue catalogue.json --out out/ (or ORDERS_INPUT, ORDERS_CATALOGUE, ORDERS_OUT): writes out/report.txt and out/rejects.csv, prints the totals block to stdout, logs JSON events (orders.read, orders.rejected per order with code, orders.done). Exit 0 none rejected, 4 some rejected, 8 fatal (unreadable or invalid JSON files, config error) with one stderr line.
Demo
The tutor wires python/py300/demo/orders/cli/main.py through Settings, the use case and renderers, runs the sample to exit 4, a clean file to 0, a missing catalogue to 8, and pipes stderr through json.tool.
Hands-on
Complete s8/orders/cli/main.py and __main__.py as specified; s8/tests/test_cli.py calls main([...]) over tmp_path copies for exit 0, 4 and 8 (missing input, missing catalogue, invalid JSON) and asserts report.txt and rejects.csv exist after a 4. At least 6 tests.
Explain
Why is a partially rejected batch exit 4 and not 0 with a note?
Check
The CLI honours 0/4/8 with six tests — checked (T3):
seat_fileseat_runseat_fileseat_test8.6Lab: Orders on inputs you did not see10 minproven · T4
Predict
A hidden file contains 500 orders, all valid, with subtotals clustered around 100.00. What is it probing?
Lesson
The placement check: the grader runs the published five cases (standard, clean, empty, all-rejected, missing catalogue) and hidden files (boundary-heavy, large, a file with an unknown sku in every order), comparing report.txt and rejects.csv byte for byte with a fixed clock, plus hidden tests against validate, price and render. basedpyright clean; no float money.
Demo
The tutor runs the reference over the five published cases from samples/s8/cases/ with ORDERS_CLOCK fixed, shows expected files matching, then describes the hidden cases without showing them.
Hands-on
Make all five published cases pass byte for byte with ORDERS_CLOCK=2026-01-15T09:00:00+00:00 (report.txt, rejects.csv, exit code), keep the suite green (at least 35 tests), basedpyright clean on s8/orders. Then ask the tutor to check: hidden files and tests run.
Explain
Name one thing in your design that made the hidden cases unsurprising.
Check
Orders passes the published cases, hidden files and hidden tests — proven (T4):
seat_runseat_testseat_lintcraft
Outcome
By the end of the course the tutor's checks will have verified: a layered service skeleton, a concurrent fetcher with deadlines, a repository with contract tests over sqlite3 and a fake, an HTTP API tested in-process, a suite with golden and property-style tests, a pipeline measured and sped up with the proof recorded, a written review that finds every planted defect, and the Orders capstone whose report, reject file and exit codes 0/4/8 match the published and hidden expected values.
Certificate
Agile System Software Certificate — Python, advanced (PY300) attests that you can design, build, persist, expose, measure, review and prove a production-shaped Python service — layered, concurrent under deadlines, tested at every level, with an exit-code contract — verified by a machine from your own seat.
To earn it:
- Pass every session lab check (1.6, 2.6, 3.6, 4.6, 5.6, 6.6, 7.6) on your own code.
- Pass the Orders capstone (8.6) including hidden input files and hidden tests.
- Score 80% or better on the 30-question reading quiz.
Gates (step · tier required):
Plus a 30-question reading quiz at 80%, retake after 48 h. Certificates are issued once the seat check runner is live; until then every step records what the engine saw.
Environment
A VS Code seat with Python 3.13, pytest, basedpyright and the standard library only (asyncio, sqlite3, http.server, cProfile, tracemalloc), offline; no third-party frameworks. Your work lives in python/py300; the tutor's demo files live apart from yours and the tutor never edits your hands-on file while you are working on it.
Open Coding Tutor at codingtutor.agilesystemsoftware.com, pick PY300 from the course button in the chat and press Tutor me. Every lesson, demo and hands-on step is also published below.