Hands-on Python exercises that check themselves with pytest —
designed to be worked through with Claude Code acting as your personal tutor and reviewer.
Every exercise ships with a locked test file.
When pytest passes, you've solved it. ✅
- 📝 Each directory in
exercises/is a self-contained lesson with tasks marked# TODO: your code herein the main.pyfile. - 🔴 A fresh exercise fails at test time —
AttributeError: module 'basics' has no attribute 'greet'literally names what you still need to build. - 🟢 You write code, re-run the test, and work from missing-attribute errors to failing assertions to green.
- 🤖 Claude reviews your work: not just "do the tests pass", but whether it's idiomatic Python.
# check a single exercise (do this constantly while working)
pytest exercises/stage1/01_basics/
# run everything (only clean once ALL exercises are solved)
pytest exercises/Note
The test_*.py file in each exercise is the spec — don't modify it.
Unlike Go or TypeScript, Python has no compiler to pin function signatures ahead of time —
that's what mypy --strict is for (see Ground rules). A fresh exercise file
has no stub at all, just the # TODO comment, so tests fail one at a time with a clear
AttributeError naming exactly what's missing.
Requires Python 3.12+.
git clone <this-repo>
cd python-training
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements-dev.txt
pytest solutions/ # sanity check: everything greenThis repo ships with a CLAUDE.md that turns Claude Code into a Python tutor. Start a session in the repo root:
claudeThen drive the training conversationally:
| 💬 You say | 🤖 Claude does |
|---|---|
| "Where should I start?" / "What's next?" | Checks your progress across all exercises and points you at the right one |
| "Explain task 8.2, I don't get it" | Teaches the concept with fresh examples — without handing you the answer |
| "I'm stuck on task 11.3" | Gives escalating hints, smallest first |
| "Review exercise 6" | Runs the tests (+ mypy --strict, ruff), reads your code, gives feedback |
| "Show me the solution for task 9.4" | Only then reveals the reference solution |
Claude is instructed to never solve exercises for you unless you explicitly ask.
You can of course also train without Claude — solve the TODOs, run the tests, and compare against solutions/ yourself.
The three stages mirror how Python proficiency develops for someone who already thinks in a statically-typed language: first the mechanics (values, functions, collections — and where Python quietly diverges from Go/TS), then Python's way of modeling (classes, dataclasses, structural typing, exceptions), then its power features (generics, concurrency, boundaries).
Each exercise comes with official reading — read it up front or in parallel, then let the tests check your understanding. All links go to docs.python.org (the CPython project's own documentation) or peps.python.org for the handful of features defined by a PEP rather than a docs page. Before Stage 1, skim the Python Tutorial once — everything below builds on it.
The mechanics you need for everything else — including the traps that bite people coming from a language with a compiler and real pointers: Python has no zero values, mutable default arguments are shared across calls, and "pass by reference vs. value" is really "mutable vs. immutable type."
| Exercise | Topics | 📖 Official reading |
|---|---|---|
| 01_basics | Type-hinted variables, f-strings, the UPPER_CASE constant convention, no automatic zero values, / vs // division |
Tutorial: An Informal Introduction to Python · Language Reference: Assignment statements |
| 02_functions | Default parameters, *args/**kwargs, closures (nonlocal), functions as values |
Tutorial: More Control Flow Tools — defining functions |
| 03_collections | List/dict/set comprehensions, dict.get as the comma-ok idiom, the list-sharing mutation trap |
Tutorial: Data Structures · Library: dict |
| 04_mutability | is vs ==, the mutable-default-argument footgun, shallow vs. deep copy, mutable vs. immutable pass semantics |
Library: copy — shallow and deep copy · Glossary: mutable · Programming FAQ (search "changing list") |
How Python structures programs without Go's implicit-everything-by-value structs or TS's nominal implements. Plain classes first, then @dataclass for the ergonomic case, then typing.Protocol — Python's structural typing, the closest thing to Go's implicit interface satisfaction — and finally exceptions, which replace (value, error) returns entirely.
| Exercise | Topics | 📖 Official reading |
|---|---|---|
| 05_classes | __init__, __repr__/__eq__ (no free structural equality), class vs. instance attributes, composition |
Tutorial: Classes · Language Reference: Data model (dunder methods) |
| 06_dataclasses | @dataclass, frozen=True, @property, mutating methods |
Library: dataclasses |
| 07_protocols | typing.Protocol structural typing, __str__, isinstance narrowing over object |
Library: typing.Protocol · Glossary: duck-typing |
| 08_exceptions | Raising ValueError, custom exception classes, raise ... from ... chaining, a retry() helper |
Tutorial: Errors and Exceptions · Library: Built-in Exceptions |
Python's answers to Go's headline features. Generics first (PEP 695's native [T] syntax), then concurrency in two steps — shared memory with threading.Lock (10), then asyncio's cooperative, message-passing style via Queue/gather (11) — and finally JSON, where untyped outside data enters your typed world.
| Exercise | Topics | 📖 Official reading |
|---|---|---|
| 09_generics | PEP 695 generic functions/classes (def first[T](...), class Stack[T]), bounded type parameters |
PEP 695 — Type Parameter Syntax · What's New in Python 3.12 (generic syntax section) |
| 10_threading | threading.Thread + join-all, threading.Lock-guarded shared state, disjoint-index parallelism |
Library: threading |
| 11_asyncio | asyncio.Queue producer/consumer, asyncio.gather fan-in, asyncio.wait_for timeouts |
Library: Coroutines and Tasks (gather, wait_for) · Queues |
| 12_json | Dataclass ⇄ JSON via json.dumps/json.loads, omitempty-style filtering, unknown-shape JSON via dict[str, Any] |
Library: json |
Tip
Work through the exercises in order — concepts are deliberately reused: mutability rules (03, 04) explain why frozen dataclasses matter (06), structural typing (07) is what makes the generic Stack[T] (09) easy to reason about, and the Lock discipline from threading (10) motivates why asyncio (11) sidesteps locks entirely by being single-threaded.
- 🎨 Run
ruff format— formatting is not a matter of taste. (ruff format --check .must print nothing.) - 🔎 Run
ruff check— catches unused imports, shadowing, and other lint issues. - 🏷️ Full type hints, checked with
mypy --strict. No untypeddefs, no unannotatedAny. - 🔒 Don't touch the
test_*.pyfiles. They're the spec. - 🚫 No bare
except:. Catch specific exception types — swallowing everything hides bugs.
Reference solutions live in solutions/, mirroring the exercise structure. Spoilers! Try the exercise, ask Claude for hints, and only then compare.
exercises/ ← your workspace: 12 exercises across 3 stages
solutions/ ← reference solutions (same layout, same tests)
CLAUDE.md ← tutor instructions for Claude Code
Makefile ← check-solutions / check-all / fmt / lint / typecheck
pyproject.toml ← pytest / mypy / ruff configuration
requirements-dev.txt ← pytest, mypy, ruff
Found a bug, an unclear task, or an idea for a new exercise? Suggestions are welcome — open an issue or start a discussion, and PRs are always welcome too.
Made for learning. Fork it, train with it, share it. 🚀
Looking for the same course for Go or TypeScript? → go-training · typescript-training