Skip to main content

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_count computed from body (not passed in)
  • .summarize() -> str: returns first 50 characters of body followed by ...
  • .add_tag(tag: str) -> None: adds tag, raises ValueError if duplicate
  • .remove_tag(tag: str) -> None: removes tag, raises ValueError if 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:

  1. What parameters does __init__ take? The dataclass took word_count as a parameter. The new class computes it from body. So __init__ takes title, body, and optionally author.
  2. Where do tags live? Lesson 4 taught you: mutable defaults go in __init__, not as class attributes.
  3. What does summarize return 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 nameWhat it proves
1test_init_sets_title_and_bodyBasic initialization stores values
2test_init_computes_word_countword_count is computed from body, not passed in
3test_init_default_authorAuthor defaults to "Anonymous"
4test_init_empty_title_raisesEmpty string title raises ValueError
5test_init_tags_emptyNew note starts with empty tag list
6test_summarize_long_bodyBody longer than 50 chars returns first 50 + "..."
7test_summarize_short_bodyBody of 50 chars or fewer returns body as-is
8test_add_tagAdding a tag makes has_tag return True
9test_add_tag_duplicate_raisesAdding the same tag twice raises ValueError
10test_remove_tagRemoving a tag makes has_tag return False
11test_remove_tag_missing_raisesRemoving a tag that does not exist raises ValueError
12test_has_tag_falsehas_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
OutcomeWhat to do
All GREENMove to Step 6
Some tests REDMove to Step 5
Pyright errorsRe-prompt: "Fix the type errors in smartnotes_class.py. Keep the logic."

Step 5: Debug

If tests failed, apply the loop from Chapter 56:

  1. Read the failure message. Which test? What was expected vs actual?
  2. Is the bug in the implementation or the test? (Your tests should be correct since you wrote them against the spec.)
  3. Paste the failure output into Claude Code and ask it to fix the specific issue.
  4. 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:

  1. How does it validate the title? Does it check if not title or if title == ""? What would happen with a title that is only whitespace?
  2. How does it compute word_count? Does it use len(body.split()) or something else? What would " hello world ".split() return?
  3. Where are tags defined? In __init__ with self, 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

Opening Claude Code

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