Capstone: Building a Real CLI Tool
🎬 Capstone: Building a Real CLI Tool
rust3-6), a task-tracker CLI built from the equivalent Rust pieces.
📋 The Data Model
@dataclass is a small, genuinely useful payoff of Course 2, Chapter 2's dunder methods: instead of hand-writing __init__, __repr__, and __eq__ the way that chapter's Point class did manually, @dataclass generates all three automatically from the type-hinted attributes above — id: int, title: str, done: bool = False reuse this course's own Chapter 5 type-hint syntax directly, now doing real work rather than just documentation.
💾 Storage: A TaskStorage Class
TaskStorage composes a Path (Course 2, Chapter 4) rather than inheriting from anything — the same composition-over-inheritance instinct Course 3, Chapter 1 recommended. load() reuses pathlib's .exists()/.read_text() and json.loads() (Course 2, Chapter 7) together; Task(**item) unpacks each JSON object's keys directly as keyword arguments into the dataclass. save() mirrors it with .write_text() and json.dumps().
⌨️ The CLI Interface: argparse
argparse — a new tool for this course, part of the standard library — parses command-line arguments into a structured args object. add_subparsers gives each subcommand (add, list, complete) its own arguments; args.command then drives a plain if/elif chain, the same branching from Course 1, Chapter 3, dispatching to whichever behavior the user asked for.
📦 Packaging It Up
This is Chapter 6's pyproject.toml, with one new piece: [project.scripts] registers a console entry point — after pip install -e ., typing tasks add "Buy milk" at the command line runs main() from tasks/cli.py directly, no python prefix needed. This is what turns a script into a genuine installed command.
🧪 Testing the Tool
tmp_path is a built-in pytest fixture (Course 2, Chapter 8) — no @pytest.fixture needed to define it, pytest provides it automatically, giving each test a fresh temporary directory so tests never touch a real ~/.tasks.json file. loaded == original works cleanly on the very first try because @dataclass already generated __eq__ — the exact custom equality Course 2, Chapter 2 wrote by hand for Fraction, here provided for free.
Task Tracker: Python vs Rust (rust3-6)
| Piece | Rust (rust3-6) | Python (this chapter) |
|---|---|---|
| Data model | A Task struct with #[derive(...)] macros | A Task dataclass with @dataclass |
| Storage abstraction | A Storage trait, implemented for a JSON backend | A TaskStorage class, composing a Path |
| Serialization | serde crate | json module (standard library) |
| CLI parsing | clap crate | argparse (standard library) |
| Testing | cargo test, #[test] | pytest, test_* functions |
The shape of the two capstones is genuinely the same problem solved with each language's own idioms — Rust reaches for third-party crates (serde, clap) for JSON and CLI parsing where Python's standard library already covers both natively, a real, concrete instance of the "batteries included" difference this whole course has touched on since Chapter 6.
@dataclass
Auto-generates __init__/__repr__/__eq__ from type-hinted attributes.
Composition
TaskStorage holds a Path, rather than inheriting from anything.
argparse subcommands
add_subparsers() gives each command (add/list/complete) its own arguments.
tmp_path fixture
A built-in pytest fixture — isolates file-touching tests automatically.
💻 Coding Challenges
Challenge 1: Add a delete Command
Extend the argparse setup with a delete subcommand taking an id, and the matching elif branch that removes the task with that ID from the list (using a list comprehension, Course 1/2) before calling storage.save().
Goal: Practice extending a real, structured CLI tool with a new command, following the existing pattern.
Challenge 2: Test the complete Behavior
Write a pytest test using tmp_path that saves two tasks, "completes" one by directly mutating its done attribute and saving again, reloads, and asserts exactly one task now has done == True.
Goal: Practice testing behavior beyond a simple round trip, reusing the tmp_path fixture pattern.
Challenge 3: A Custom Exception for a Missing Task
Define a custom exception TaskNotFoundError(Exception) (Course 2, Chapter 3). Write a method TaskStorage.find(tasks, task_id) that returns the matching Task or raises TaskNotFoundError with a descriptive message if no task has that ID.
Goal: Tie the capstone back to this course's own exception-handling chapter with a realistic use case.
🎓 Course Complete!
That's all 8 chapters of Python Advanced — advanced OOP, iterators and context managers, concurrency, async, type hints, packaging, performance, and this capstone — and with it, the full three-course Python track (Fundamentals → Intermediate → Advanced, 25 chapters). Python Projects (py4) remains outlined as a lighter, project-based fourth course whenever you're ready to continue.