SmartNotes Class TDG
Emma pulls up the Note dataclass that James has been using since Chapter 51:
@dataclass
class Note:
title: str
body: str
word_count: int
author: str = "Anonymous"
is_draft: bool = True
tags: list[str] = field(default_factory=list)
"You have all the pieces," she says. "You know __init__ and self from Lesson 2. You know methods from Lesson 3. You know instance vs class attributes from Lesson 4. Convert this to a full class. Use TDG."
She writes the requirements on the whiteboard:
__init__with validation: title cannot be empty,word_countcomputed from body (not passed in).summarize() -> str: returns first 50 characters of body followed by....add_tag(tag: str) -> None: adds tag, raisesValueErrorif duplicate.remove_tag(tag: str) -> None: removes tag, raisesValueErrorif not found.has_tag(tag: str) -> bool: returns whether note has the tag
She sets a timer. "Twenty-five minutes. You drive."
Step 1: Write the Class Interface
Open a new file smartnotes_class.py. Write the class with method signatures, type annotations, and docstrings. Every method body is ... (the stub). The __init__ body is also ... for now.
Three design decisions to make before you type:
- What parameters does
__init__take? The dataclass tookword_countas a parameter. The new class computes it frombody. So__init__takestitle,body, and optionallyauthor. - Where do
tagslive? Lesson 4 taught you: mutable defaults go in__init__, not as class attributes. - What does
summarizereturn for a body shorter than 50 characters? Your docstring should specify this.
Hint: the class skeleton
class Note:
"""A note with validation, computed word count, and tag management."""
def __init__(self, title: str, body: str, author: str = "Anonymous") -> None:
"""Initialize a Note. Raises ValueError if title is empty.
word_count is computed from body. tags starts as an empty list."""
...
def summarize(self) -> str:
"""Return first 50 characters of body followed by '...'.
If body is 50 characters or fewer, return body as-is."""
...
def add_tag(self, tag: str) -> None:
"""Add a tag. Raises ValueError if tag already exists."""
...
def remove_tag(self, tag: str) -> None:
"""Remove a tag. Raises ValueError if tag not found."""
...
def has_tag(self, tag: str) -> bool:
"""Return True if the note has the given tag, False otherwise."""
...
Run pyright on your stub:
uv run pyright smartnotes_class.py
Fix any type errors before moving on. The stub is your specification. It must be type-correct.
Step 2: Write the Tests
Open test_smartnotes_class.py. Write at least 10 tests. Each test proves one specific behavior.
Here is the test list. Write them in order:
| # | Test name | What it proves |
|---|---|---|
| 1 | test_init_sets_title_and_body | Basic initialization stores values |
| 2 | test_init_computes_word_count | word_count is computed from body, not passed in |
| 3 | test_init_default_author | Author defaults to "Anonymous" |
| 4 | test_init_empty_title_raises | Empty string title raises ValueError |
| 5 | test_init_tags_empty | New note starts with empty tag list |
| 6 | test_summarize_long_body | Body longer than 50 chars returns first 50 + "..." |
| 7 | test_summarize_short_body | Body of 50 chars or fewer returns body as-is |
| 8 | test_add_tag | Adding a tag makes has_tag return True |
| 9 | test_add_tag_duplicate_raises | Adding the same tag twice raises ValueError |
| 10 | test_remove_tag | Removing a tag makes has_tag return False |
| 11 | test_remove_tag_missing_raises | Removing a tag that does not exist raises ValueError |
| 12 | test_has_tag_false | has_tag returns False for a tag not on the note |
Hint: test structure
import pytest
from smartnotes_class import Note
def test_init_sets_title_and_body() -> None:
note = Note("Test Title", "This is the body")
assert note.title == "Test Title"
assert note.body == "This is the body"
def test_init_computes_word_count() -> None:
note = Note("Test", "one two three four five")
assert note.word_count == 5
def test_init_empty_title_raises() -> None:
with pytest.raises(ValueError):
Note("", "Some body text")
Write the remaining tests yourself before looking at the next hint.
Hint: testing summarize
def test_summarize_long_body() -> None:
long_body = "a" * 100
note = Note("Test", long_body)
assert note.summarize() == "a" * 50 + "..."
def test_summarize_short_body() -> None:
note = Note("Test", "Short body")
assert note.summarize() == "Short body"
Run the tests. Every test should FAIL:
uv run pytest test_smartnotes_class.py -v
If any test passes against a stub, your test is not calling the method correctly. Fix it.
Commit both files before you continue:
git add smartnotes_class.py test_smartnotes_class.py
git commit -m "Add Note class stub and test suite (RED)"
Step 3: Prompt AI to Implement
In Claude Code, type:
Implement the Note class in smartnotes_class.py so that every test
in test_smartnotes_class.py passes. Do not modify the test file.
Keep the method signatures and docstrings exactly as they are.
The stub has the types. The docstrings have the behavior. The tests have the acceptance criteria. That is the complete specification.
Hint: if the AI asks questions
If the AI asks how to compute word_count, the answer is in your docstring. If the AI asks about edge cases, the answer is in your tests. A well-written stub and test suite should eliminate most questions. If the AI is confused, your specification may be incomplete. Go back and improve the docstring.
Step 4: Run the Verification Stack
uv run ruff check smartnotes_class.py
uv run pyright smartnotes_class.py
uv run pytest test_smartnotes_class.py -v
| Outcome | What to do |
|---|---|
| All GREEN | Move to Step 6 |
| Some tests RED | Move to Step 5 |
| Pyright errors | Re-prompt: "Fix the type errors in smartnotes_class.py. Keep the logic." |
Step 5: Debug
If tests failed, apply the loop from Chapter 56:
- Read the failure message. Which test? What was expected vs actual?
- Is the bug in the implementation or the test? (Your tests should be correct since you wrote them against the spec.)
- Paste the failure output into Claude Code and ask it to fix the specific issue.
- Run the stack again.
Document each iteration in a tdg_journal.md:
# TDG Journal: Note Class
## Iteration 1
- Prompt: "Implement the Note class..."
- Result: 10/12 tests passed
- Failures:
- test_add_tag_duplicate_raises: no ValueError raised
- test_init_empty_title_raises: no ValueError raised
- Re-prompted with failure output
## Iteration 2
- Result: 12/12 tests passed
Repeat until all tests are GREEN.
Step 6: Read the Generated Code
All tests pass. Now apply PRIMM.
Open smartnotes_class.py and read the __init__ method line by line:
- How does it validate the title? Does it check
if not titleorif title == ""? What would happen with a title that is only whitespace? - How does it compute
word_count? Does it uselen(body.split())or something else? What would" hello world ".split()return? - Where are
tagsdefined? In__init__withself, or as a class attribute? (You know from Lesson 4 why this matters.)
Read add_tag. Does it check for duplicates before or after appending? What happens if you pass an empty string as a tag?
These are the questions the AI cannot answer for you. The tests prove correctness for the inputs you specified. Your review catches the cases you did not test.
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 the Implementation
Here is my Note class implementation:
[paste the generated smartnotes_class.py]
Review this code:
1. Is word_count computed correctly for edge cases like
empty body, body with multiple spaces, body with newlines?
2. Does add_tag handle empty strings? Should it?
3. What would you improve about this implementation?
What you're learning: You are reviewing AI-generated code for correctness beyond the test suite. The AI suggests improvements you did not test for. Each suggestion either becomes a new test (if valid) or gets rejected with reasoning (if unnecessary).
Prompt 2: Compare to the Dataclass
Here is the original dataclass:
@dataclass
class Note:
title: str
body: str
word_count: int
author: str = "Anonymous"
is_draft: bool = True
tags: list[str] = field(default_factory=list)
Here is my new class:
[paste your Note class]
Compare them:
1. What can the class do that the dataclass cannot?
2. What did the dataclass give you for free that you had to
write manually in the class?
3. When would you still choose a dataclass over a full class?
What you're learning: You are building a decision framework. The dataclass gives you __init__, __repr__, __eq__ for free. The full class gives you validation, computed properties, and encapsulated behavior. Neither is always better. The choice depends on what the object needs to do.
Prompt 3: Suggest Missing Tests
Here is my test suite for the Note class:
[paste test_smartnotes_class.py]
Suggest 3 tests I did not write that would catch real bugs.
For each one, explain what failure it would catch.
What you're learning: Your test suite covers the specification you wrote. The AI suggests cases you did not specify: whitespace-only titles, tags with leading/trailing spaces, bodies with only punctuation. Each suggestion reveals a gap between "what I specified" and "what could go wrong in production."
James stares at the two files side by side: the original dataclass from Chapter 51 and the new class from today. Six lines of dataclass. Forty lines of class.
"I started with a dataclass," he says. "Six fields, no behavior. The title could be empty. The word count was a number I passed in manually. The tags were a list I managed from outside." He points to the new class. "Now the title validates itself. The word count computes itself. The tags manage themselves. Every behavior lives inside the object."
"And the tests prove every behavior," Emma says. "How many?"
"Twelve. Init, validation, computed property, summarize, add, remove, has, duplicates, missing tags, defaults." He counts on his fingers. "The TDG cycle did not change. I wrote stubs, then tests, then prompted, then verified, then debugged, then read. The same six steps from Chapter 57."
"What changed?"
"The specification. In Chapter 57, I specified a function: inputs, outputs, edge cases. Here I specified a class: constructor, methods, state changes, error cases. The building blocks got bigger, but the cycle is the same."
Emma erases the whiteboard and writes:
Your Note class works alone. But SmartNotes needs different kinds of notes: text notes, code notes, link notes. Do they all get their own class? Or do they share behavior?
"That," she says, "is inheritance and composition. Chapter 59."