SmartNotes Hierarchy TDG
In Lesson 4, James classified five class relationships using one question: does B share A's interface AND is B truly a specialized version of A? He documented the answers in a table, built an abstract base class with @abstractmethod, and tested design decisions through interfaces rather than internals.
Emma opens a blank project folder and writes four lines on the whiteboard:
TextNote(Note) -- inheritance CodeNote(Note) -- inheritance LinkNote(Note) -- inheritance NoteCollection -- composition
"Four classes. Two design patterns. Fifteen tests. Use TDG." She sets a timer. "Twenty-five minutes."
James glances at the whiteboard. He has done this at the warehouse: redesigning a department meant identifying which roles specialized from a base role (inheritance) and which departments contained teams without being teams themselves (composition). The org chart used both patterns. The SmartNotes hierarchy uses both patterns.
He opens his editor.
Step 1: Design Decisions (3 minutes)
Before writing code, document your decisions. Create design_decisions.md in your project folder. For each class, apply the one-question framework from Lesson 4.
| Class | The Question | Answer | Decision | Reasoning |
|---|---|---|---|---|
TextNote / Note | Does TextNote share Note's interface AND is it a specialized Note? | YES | Inheritance | TextNote has title, body, tags, summarize, add_tag, remove_tag, has_tag. It adds a [TEXT] prefix in summarize. It can substitute for Note anywhere. |
CodeNote / Note | Does CodeNote share Note's interface AND is it a specialized Note? | YES | Inheritance | CodeNote has everything Note has, plus a language attribute. It specializes summarize to show the language. A CodeNote can substitute for a Note. |
LinkNote / Note | Does LinkNote share Note's interface AND is it a specialized Note? | YES | Inheritance | LinkNote has everything Note has, plus a url attribute. It specializes summarize to show the URL. A LinkNote can substitute for a Note. |
NoteCollection / list | Does NoteCollection share list's interface AND is it a specialized list? | NO | Composition | A list exposes reverse, insert, pop, sort, clear. NoteCollection should not expose any of them. It contains a list, it is not one. |
Write your own version of this table before proceeding. If your answers differ from these, re-read the one question and trace through it again.
Step 2: Write Stubs (5 minutes)
Create smartnotes_hierarchy.py. Write the class interfaces with typed method signatures. Every method body is .... The Note base class uses ABC and @abstractmethod on summarize, as you learned in Lesson 4.
Hint: Note base class structure
from abc import ABC, abstractmethod
class Note(ABC):
def __init__(self, title: str, body: str, author: str = "Anonymous") -> None:
...
@property
def word_count(self) -> int:
...
@abstractmethod
def summarize(self) -> str:
...
def add_tag(self, tag: str) -> None:
...
def remove_tag(self, tag: str) -> None:
...
def has_tag(self, tag: str) -> bool:
...
Hint: TextNote stub structure
class TextNote(Note):
def summarize(self) -> str:
...
TextNote needs no __init__ override. It inherits everything from Note and only overrides summarize.
Hint: CodeNote stub structure
class CodeNote(Note):
def __init__(self, title: str, body: str, language: str,
author: str = "Anonymous") -> None:
...
def summarize(self) -> str:
...
CodeNote overrides __init__ to add language and overrides summarize to show the language.
Hint: LinkNote stub structure
class LinkNote(Note):
def __init__(self, title: str, url: str, body: str = "",
author: str = "Anonymous") -> None:
...
def summarize(self) -> str:
...
LinkNote overrides __init__ to add url (with body defaulting to empty string) and overrides summarize to show the URL.
Hint: NoteCollection stub structure
class NoteCollection:
def __init__(self) -> None:
...
def add(self, note: Note) -> None:
...
def count(self) -> int:
...
def get_by_tag(self, tag: str) -> list[Note]:
...
def get_all(self) -> list[Note]:
...
NoteCollection uses composition: it stores self._notes: list[Note] internally. No inheritance from list.
Run uv run pyright smartnotes_hierarchy.py. The stubs must be type-correct. Fix any type errors before writing tests.
Step 3: Write Tests (8 minutes)
Create test_smartnotes_hierarchy.py. Write at least 15 tests. Your tests are the contract: they define correct behavior for the entire hierarchy.
Think about what "correct" means for each class:
Inheritance tests (TextNote, CodeNote, LinkNote):
- Does the child inherit parent methods? (
add_tag,has_tag,remove_tag,word_count) - Does the overridden
summarize()produce the right format? - Does
super().__init__()reuse parent validation? (Empty title raisesValueError) - Do new attributes work? (
languageon CodeNote,urlon LinkNote)
Composition tests (NoteCollection):
- Does
addincrease the count? - Does
get_by_tagreturn only matching notes? - Does
get_allreturn all added notes? - Does
NoteCollectionlack list methods likereverseorpop? - Does an empty collection return 0 for count and an empty list for get_all?
Cross-class tests:
- Can you add different note types (TextNote, CodeNote, LinkNote) to the same NoteCollection?
- Does
get_by_tagwork across mixed note types?
Hint: sample test structure
import pytest
from smartnotes_hierarchy import Note, TextNote, CodeNote, LinkNote, NoteCollection
# --- TextNote tests ---
def test_text_note_inherits_add_tag():
note = TextNote(title="Meeting", body="Q3 review")
note.add_tag("work")
assert note.has_tag("work")
def test_text_note_summarize_format():
note = TextNote(title="Meeting", body="Q3 review")
result = note.summarize()
assert "[TEXT]" in result
assert "Meeting" in result
def test_text_note_empty_title_raises():
with pytest.raises(ValueError):
TextNote(title="", body="content")
# --- NoteCollection tests ---
def test_collection_add_increases_count():
col = NoteCollection()
col.add(TextNote(title="A", body="body"))
assert col.count() == 1
def test_collection_has_no_reverse():
col = NoteCollection()
assert not hasattr(col, "reverse")
Build the rest. Aim for 15+.
Run uv run pytest test_smartnotes_hierarchy.py -v. Every test should FAIL. If any passes, your stub is returning a value instead of ..., or your test is not calling the function correctly. Fix it.
Commit both files before proceeding:
git add smartnotes_hierarchy.py test_smartnotes_hierarchy.py design_decisions.md
git commit -m "Add SmartNotes hierarchy stubs and test suite (RED)"
Step 4: Generate (3 minutes)
Open Claude Code and prompt:
Implement all classes in smartnotes_hierarchy.py so that every
test in test_smartnotes_hierarchy.py passes. Do not modify the
test file. Keep the ABC and @abstractmethod on Note.
The stubs have the types. The docstrings have the behavior. The tests have the acceptance criteria. The AI has everything it needs.
Hint: what if the AI changes your class signatures?
If the AI modifies your method signatures or removes @abstractmethod, re-prompt:
Keep the exact class signatures I wrote. Do not remove ABC or
@abstractmethod. Only fill in the method bodies.
Your stubs are the specification. The AI implements to your spec, not its own.
Step 5: Verify (3 minutes)
Run the verification stack in order:
uv run ruff check smartnotes_hierarchy.py
uv run pyright smartnotes_hierarchy.py
uv run pytest test_smartnotes_hierarchy.py -v
Three possible outcomes:
| Outcome | What it means | What to do |
|---|---|---|
| All GREEN | Every test passes, types are clean, lint is clean | Move to the closing |
| Some RED | One or more tests fail | Move to Step 6 (Debug) |
| Pyright errors | AI broke the types or removed @abstractmethod | Re-prompt: "Fix the type errors. Keep the ABC structure." |
Step 6: Debug (3 minutes)
If tests failed, run the debugging loop from Chapter 57:
- Read the failure message. Which test? What was expected vs. actual?
- Decide: implementation bug or test bug? If your test expects the wrong format string, the test is wrong. If the implementation returns wrong output for a correct test, the implementation is wrong.
- Re-prompt or fix manually. Paste the failure output into Claude Code.
- Run the stack again. Repeat until GREEN.
Hint: common failures for hierarchy capstones
- Missing
super().__init__()call: CodeNote or LinkNote does not call the parent's__init__, soself.tagsis never created.add_tagraisesAttributeError. - Wrong
summarizeformat: The AI uses a different format string than your tests expect. Check whether your tests assert on[TEXT],[PYTHON],[LINK]prefixes. - NoteCollection exposes list methods: If the AI inherits from
listinstead of using composition, yourtest_collection_has_no_reversefails. Re-prompt: "NoteCollection must use composition, not inherit from list." get_by_tagmisses notes: The AI may checktag in note.tagswithout handling case sensitivity. Check your test expectations.
Document each iteration in tdg_journal.md:
# TDG Cycle Journal: SmartNotes Hierarchy
## Iteration 1
- Prompt: "Implement all classes..."
- Result: 13/15 tests passed
- Failures:
- test_collection_has_no_reverse: AI inherited from list
- test_code_note_summarize_format: wrong format string
- Fix: Re-prompted with failure output and constraint
## Iteration 2
- Result: 15/15 tests passed
- All ruff/pyright/pytest GREEN
Try With AI
If Claude Code is not already running, open your terminal, navigate to your SmartNotes project folder, and type claude. If you need a refresher, Chapter 44 covers the setup.
Prompt 1: Review My Design Decisions
Here are my design decisions for SmartNotes:
- TextNote inherits from Note (is-a: specialized note type)
- CodeNote inherits from Note (is-a: adds language)
- LinkNote inherits from Note (is-a: adds url)
- NoteCollection uses composition (has-a: contains a list of Note)
Evaluate each decision. For any that you disagree with,
explain what you would change and why. For any you agree
with, describe one scenario where the decision might need
to change.
What you're learning: Design decisions are context-dependent. The AI might agree with your inheritance choices but point out that a deep hierarchy (TextNote -> RichTextNote -> FormattedRichTextNote) would change the answer. Stress-testing your decisions builds design judgment that outlasts any single project.
Prompt 2: What If I Used Inheritance for NoteCollection?
I built NoteCollection using composition: it contains a
list[Note] internally. What would happen if I inherited
from list instead? Show me the class both ways and explain
what breaks when I use inheritance.
What you're learning: The AI shows you the concrete consequences of the wrong design decision. When NoteCollection inherits from list, users can call .reverse(), .pop(), and .clear() on your collection, bypassing any validation you built. Seeing the failure makes the "when in doubt, composition" rule concrete instead of abstract.
Prompt 3: Rate My Test Coverage
Here is my test suite for the SmartNotes hierarchy:
[paste test file]
Rate my coverage. What behaviors am I testing well?
What gaps do you see? Suggest 3 tests I should add
and explain what each one would catch.
What you're learning: No test suite is complete. The AI reviews your tests the way a senior developer would during a code review: looking for missing edge cases, untested interactions between classes, and assumptions you encoded without realizing. The goal is expanding your instinct for what "correct" means across a multi-class system.
James commits the final version: four classes, fifteen tests, all green. He closes the TDG journal and looks at the whiteboard where Emma's four lines still stand.
"I learned three things this chapter," he says. "When to inherit, when to compose, and how to decide."
Emma tilts her head. "Four things. You also learned how to test the decision. Your test for not hasattr(col, 'reverse') does not check the implementation. It checks the design. That is an interface test for a design decision. Most developers never write that test."
James thinks about that. "In the warehouse, we had an inspection checklist for every new process. One item was always: 'does this process expose operations the operator should not have access to?' If a machine's control panel had buttons the operator should never press, we covered them with a locked panel. The composition pattern is the locked panel. The test verifies the panel is locked."
"That is exactly right." Emma pauses, the way she does when she is about to admit something. "I skipped that test on my first composition class. Inherited from list because it was faster. A colleague called .sort() on my collection in production and reordered a priority queue. Took two hours to find the bug. I write the not hasattr test on every composition class now."
She erases the whiteboard and writes:
Your classes work. Your hierarchy is clean. But print(note) shows <__main__.TextNote object at 0x...>. Your objects do not know how to display themselves, compare themselves, or participate in for loops. That is special methods. Chapter 60.