Agile System SoftwareLearn

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.

  • LevelAdvanced
  • Durationself-paced ~40–50 h
  • Size8 sessions · 48 mini steps · ~425 min of lessons
  • EnvironmentA 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
  • PrerequisitesPY100, or test out with its placement step 8.6. Comfortable with dataclasses, exceptions, pytest and sqlite3.
    Courses: PY100
  • CertificateAgile System Software Certificate — Python, advanced (PY300)

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).

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. 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_file seat_file seat_run

    #s1-1

  2. 1.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_file seat_lint seat_run

    #s1-2

  3. 1.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_file seat_run

    #s1-3

  4. 1.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_file seat_run

    #s1-4

  5. 1.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_file seat_test craft

    #s1-5

  6. 1.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_run seat_run seat_file seat_lint

    #s1-6

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
  1. 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_file seat_run

    #s2-1

  2. 2.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_file seat_run

    #s2-2

  3. 2.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_file seat_run

    #s2-3

  4. 2.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_file seat_run

    #s2-4

  5. 2.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_file seat_run

    #s2-5

  6. 2.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_file seat_test seat_lint

    #s2-6

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
  1. 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_file seat_run

    #s3-1

  2. 3.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_file seat_run

    #s3-2

  3. 3.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_file seat_run craft

    #s3-3

  4. 3.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_file seat_test craft

    #s3-4

  5. 3.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_file seat_test

    #s3-5

  6. 3.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_file seat_test seat_lint craft

    #s3-6

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
  1. 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_file seat_run

    #s4-1

  2. 4.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_file seat_test

    #s4-2

  3. 4.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_file seat_test

    #s4-3

  4. 4.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_file seat_test

    #s4-4

  5. 4.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_file seat_test

    #s4-5

  6. 4.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_file seat_test seat_lint craft

    #s4-6

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
  1. 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_file seat_file seat_test

    #s5-1

  2. 5.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_file seat_test

    #s5-2

  3. 5.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_file seat_file seat_test

    #s5-3

  4. 5.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_file seat_file seat_test

    #s5-4

  5. 5.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_file seat_file seat_test

    #s5-5

  6. 5.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_test seat_file craft

    #s5-6

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
  1. 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_file seat_run seat_file

    #s6-1

  2. 6.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_file seat_file seat_file

    #s6-2

  3. 6.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_file seat_test seat_file

    #s6-3

  4. 6.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_file seat_run seat_file seat_file

    #s6-4

  5. 6.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_file seat_run seat_file

    #s6-5

  6. 6.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_run seat_test seat_file seat_file

    #s6-6

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
  1. 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_file

    #s7-1

  2. 7.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_file seat_test

    #s7-2

  3. 7.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_file

    #s7-3

  4. 7.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_test seat_file

    #s7-4

  5. 7.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_file seat_test seat_file

    #s7-5

  6. 7.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_test seat_test seat_file

    #s7-6

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
  1. 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_file

    #s8-1

  2. 8.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_file seat_test

    #s8-2

  3. 8.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_file seat_test craft

    #s8-3

  4. 8.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_file seat_test

    #s8-4

  5. 8.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_file seat_run seat_file seat_test

    #s8-5

  6. 8.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_run seat_test seat_lint craft

    #s8-6

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.