Day 10 Β· Family trees vs Lego

Object-Oriented Python II

You will be able to
  • 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
Today's ~120 minutes
Spaced-rep warm-up: due cards (Day 9 dunders will appear)10 min
Concept study: ELI5 + tech β€” say the is-a/has-a sentences aloud20 min
Guided: family tree, dataclasses, Lego assembly45 min
Practice: refactor the lying hierarchy20 min
Project: tracker v315 min
Quiz + flashcards10 min

Builds on: Day 9 β€” Classes, self, dunder methods Β· Day 4 β€” Collections (composition builds on them)

The analogy

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.

Why this matters on the job

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

guided 1

The family tree β€” Notifier and its children

15 min
  1. Create week-02/notifiers.py with the starter code and run it. Note what SMSNotifier inherits without writing: __init__ and the ready() method, straight from the parent.
  2. 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().
  3. 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__.
  4. Check the contract: print isinstance(email, Notifier) and isinstance("hello", Notifier).
  5. 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.)
🐍 python β€” editable, runs in your browser
Ctrl/⌘+Enter runs · Tab indents · numpy/pandas/sklearn auto-load on import (torch and network calls need a local run)
guided 2

Dataclasses β€” delete your boilerplate

15 min
  1. Create dataclass_lab.py. Start by pasting your Day 9 Task class (with its handwritten __init__, __repr__, __eq__). Count its lines.
  2. 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.
  3. 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.
  4. Add a method β€” dataclasses are still classes: def complete(self): self.done = True. Methods and generated code coexist.
  5. 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.
🐍 python β€” editable, runs in your browser
Ctrl/⌘+Enter runs · Tab indents · numpy/pandas/sklearn auto-load on import (torch and network calls need a local run)
guided 3

Lego assembly β€” composition and a property

15 min
  1. Create composed_tracker.py with the starter code β€” a Tracker built the Lego way: it HAS tasks and HAS a storage brick.
  2. 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.
  3. 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.
  4. 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.
  5. 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).
🐍 python β€” editable, runs in your browser
Ctrl/⌘+Enter runs · Tab indents · numpy/pandas/sklearn auto-load on import (torch and network calls need a local run)

On your own

Refactor the family tree that lied

20 min

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

Ship before you stop

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.

Rubric β€” check what you completed (0/6)

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

docsPython Tutorial β€” 9.5 Inheritance β†—15 mindocsdataclasses β€” official docs (through field()) β†—20 mindocsBuilt-in functions β€” property (the official example is excellent) β†—10 min
If you have a third hour
  • 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.
Done means
  • 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
How this connects

← 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