Object-Oriented Python II
- Create subclasses that override methods and call up with super()
- Decide between inheritance ("is-a") and composition ("has-a") for a given design and defend the choice
- Replace boilerplate classes with dataclasses, including defaults and field
- Expose computed values as properties instead of getter methods
- Explain duck typing and why Python functions rarely check types
| Spaced-rep warm-up: due cards (Day 9 dunders will appear) | 10 min |
| Concept study: ELI5 + tech β say the is-a/has-a sentences aloud | 20 min |
| Guided: family tree, dataclasses, Lego assembly | 45 min |
| Practice: refactor the lying hierarchy | 20 min |
| Project: tracker v3 | 15 min |
| Quiz + flashcards | 10 min |
Builds on: Day 9 β Classes, self, dunder methods Β· Day 4 β Collections (composition builds on them)
There are two ways to build a new thing from existing things. The family tree way: a child inherits everything from a parent, then differs where it must. A Sheepdog *is a* Dog β it gets bark() for free and overrides herd(). That's inheritance: powerful when the family resemblance is real, miserable when it isn't β force a Robot into the Dog family for reuse of walk() and you'll spend your life apologizing for it barking.
The Lego way: snap independent pieces together. A camper van *has an* engine, *has a* kitchen, *has a* bed β nobody insists a camper van is a kind of kitchen. That's composition: your Tracker HAS a list of Tasks and HAS a storage backend; swap the storage brick for a database later and nothing else moves. The modern rule of thumb, worth tattooing somewhere: reach for Lego first, family trees only when the "is-a" sentence is genuinely true and the family shares real behavior. Python also throws in a labor-saving gift today β dataclasses, which write yesterday's __init__/__repr__/__eq__ boilerplate for you from a simple list of fields.
This is the day you learn to *judge* designs, not just write them β and design judgment is what code reviews, systems interviews, and FDE work actually test. Deep inheritance towers are the most common legacy-code pathology you'll meet in customer codebases; recognizing "this should have been composition" is a consulting skill with a billing rate. Dataclasses, meanwhile, are modern Python's default way to model data β config objects, API payloads, RAG chunks (Day 114) β and they're the on-ramp to pydantic (Day 41), which powers FastAPI and most structured-output LLM work (Day 110). Duck typing explains why Python libraries compose so freely β and why type hints (Day 15) add guardrails.
Guided practice
The family tree β Notifier and its children
15 min- Create
week-02/notifiers.pywith the starter code and run it. Note what SMSNotifier inherits without writing: __init__ and the ready() method, straight from the parent. - Trace the override: notify_all calls .send on each object; Python finds EmailNotifier's send for the first, SMSNotifier's for the second. Same call, different behavior β that's polymorphism, and you've used it since "x".upper() vs [].copy().
- Add super() practice: give EmailNotifier its own __init__ taking (name, address) that calls super().__init__(name) then sets self.address. Update the demo. Rule: child __init__ almost always starts with super().__init__.
- Check the contract: print isinstance(email, Notifier) and isinstance("hello", Notifier).
- Now the duck: add class FakeNotifier with a send(msg) that just appends to self.sent β NO inheritance from Notifier. Add it to the notify_all list. It works! Write the one-line lesson: notify_all never needed the family, only the quack. (Day 19 uses exactly this trick to test without sending real messages.)
Dataclasses β delete your boilerplate
15 min- Create
dataclass_lab.py. Start by pasting your Day 9 Task class (with its handwritten __init__, __repr__, __eq__). Count its lines. - Below it, write the dataclass version from the starter code. Count again. Run the demo: construction, printing, and == all behave like yesterday's handwritten versions β generated for you from the field list.
- Try the trap on purpose: change tags to tags: list = [] and run. Read the ValueError β dataclasses structurally BAN the shared-mutable-default bug you met on Day 9. Restore default_factory and salute the language designers.
- Add a method β dataclasses are still classes: def complete(self): self.done = True. Methods and generated code coexist.
- Convert your Day 9 Flashcard to a dataclass. One wrinkle to solve: __eq__ should ignore streak β check the dataclass docs for field(compare=False) and apply it. Ten minutes, and you now know how to steer the generated code.
Lego assembly β composition and a property
15 min- Create
composed_tracker.pywith the starter code β a Tracker built the Lego way: it HAS tasks and HAS a storage brick. - Run it and trace one save: Tracker.add calls self.storage.save β the Tracker knows WHAT to persist, the brick knows HOW. Neither knows the other's internals.
- Feel the swap: write class MemoryStorage with load()/save(data) that just keeps a list in an attribute β five lines. Build Tracker(MemoryStorage()) and confirm identical behavior with no files touched. You just did what Day 19 calls "injecting a test double" and what Day 36 will do with a database β zero Tracker edits either time.
- The property: open_count is computed from tasks on demand, but reads as an attribute (no parens). Add a second property, done_count. Why not store counts as attributes? Write the answer: stored copies drift out of sync; computed values can't.
- Contrast exercise, on paper: sketch the inheritance version (class Tracker(list)). List two concrete ways it goes wrong (hint: tracker.clear() from any caller; every list method becomes your public API whether it makes sense or not).
On your own
Refactor the family tree that lied
20 minYou inherit this design (type it out): class Report with load_data(), format_text(), and send_email() all in one class; class PdfReport(Report) overriding format_text; class SlackReport(Report) overriding send_email AND overriding format_text to raise NotImplementedError("slack uses blocks"); class CsvReport(Report) overriding send_email to do nothing because CSVs get saved, not sent.
Goal: the NotImplementedError and the do-nothing override are the tells of a lying hierarchy. Redesign with composition: a Report that HAS a formatter brick and HAS a delivery brick (each a tiny class with one method β duck-typed, no base class needed). Build three concrete combinations matching the originals, working end to end with print-based stubs.
Constraints: zero inheritance in the final design; adding a "markdown formatter delivered to slack" combination must require ONE new class and ONE changed line.
Hints (only if stuck): Report.__init__(self, formatter, delivery); run() calls self.formatter.format(data) then self.delivery.send(result).
Tracker v3 β dataclass + Lego edition
Rebuild the tracker on today's foundations, in week-02/tracker_v3/: Task as a dataclass (id, text, done, tags with default_factory, optional due_day); a JsonStorage brick owning ALL file I/O; a Tracker class composing storage + tasks with add/complete/delete/rename methods and two properties (open_count, done_count); the CLI loop from Day 7 driving it, now three thin layers (interface β Tracker β storage). Then prove the Lego: add MemoryStorage and a --demo flag (or a DEMO constant) that runs the tracker against it, touching no files. Commit in logical steps as usual. Write three sentences in journal.md: one design decision you made, one thing composition made easier, one thing that felt like overkill at this size β honest judgment is the skill.
Common mistakes & misconceptions
- Inheriting to reuse code when the "is-a" sentence is false (Tracker extends list, Robot extends Dog). You inherit the parent's entire public API and its future changes. Compose instead; delegate the one method you wanted.
- Forgetting super().__init__ in a subclass __init__ β the parent's attributes never get created, and errors surface later, far from the cause.
- Overriding a method to raise NotImplementedError or pass. That's the hierarchy confessing it's wrong: the child was never really a parent. Restructure.
- Writing tags: list = [] in a dataclass. It refuses to run β by design. Use field(default_factory=list) and be grateful the Day 9 trap is now a compile-time error.
- Storing computed values as attributes and updating them manually (self.open_count everywhere). They drift out of sync. Compute them in a @property.
- isinstance-checking every argument "for safety." Duck typing is the design: document the behavior you need, accept anything providing it β your notify_all took a test fake precisely because it did NOT check.
Q1. A CachedStorage should reuse JsonStorage's behavior but keep recent reads in memory. Best structure?
Q2. What does @dataclass generate from the field list?
Q3. Why does notify_all(notifiers, msg) work on a FakeNotifier that inherits from nothing?
Go deeper β curated resources
- Composition over inheritance β the classic argument β The principle predates Python (Gang of Four, 1994: "favor object composition over class inheritance"). Search the phrase and read one good essay; then reread your practice refactor and notice you rediscovered every point yourself.
- Notifier lab run including the duck-typed FakeNotifier
- Dataclass lab done; the banned-mutable-default error triggered and understood
- MemoryStorage swap works with zero Tracker changes
- Practice refactor passes the one-class-one-line extensibility test
- Tracker v3 committed; journal judgment written; quiz β₯ 2/3
β Back: Dataclasses automate exactly the __init__/__repr__/__eq__ you handwrote on Day 9 β and their default_factory rule is the language fixing Day 4's aliasing trap. The storage brick is Day 5's JSON code, given a wall around it.
Forward β: The @dataclass and @property decorators get demystified on Day 12 (you'll write your own). The swappable-brick move returns as dependency injection on Day 45 and test doubles on Day 19. pydantic (Day 41) is dataclasses with validation; Protocol (Day 15) formalizes today's ducks.
Unlocks: D12 Closures, Decorators & Functional Style Β· D14 Week 2 Checkpoint: Log Analyzer Β· D41 HTTP & Build Your First API