Decorators for SmartNotes
James understands the mechanism: @decorator means func = decorator(func). Now he wants to write one that solves a real problem. Every method on Note that accepts a string should reject empty strings. He could add an if not tag.strip() check inside every method, but that is the same duplication that led him to decorators in the first place.
"Write the validation once," Emma says. "Apply it everywhere."
Writing @validate_input for SmartNotes
Now write a decorator that validates string arguments are non-empty:
import functools
def validate_input(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for arg in args[1:]: # skip self (first argument)
if isinstance(arg, str) and not arg.strip():
raise ValueError(
f"{func.__name__}: empty string argument"
)
return func(*args, **kwargs)
return wrapper
Apply it to Note methods:
class Note:
def __init__(self, title: str, body: str, author: str = "Anonymous") -> None:
self.title = title
self.body = body
self.author = author
self.tags: list[str] = []
@validate_input
def add_tag(self, tag: str) -> None:
if tag not in self.tags:
self.tags.append(tag)
@validate_input
def remove_tag(self, tag: str) -> None:
if tag not in self.tags:
raise ValueError(f"Tag '{tag}' not found")
self.tags.remove(tag)
note = Note("Test", "Body")
note.add_tag("python")
print(note.tags)
note.add_tag("") # raises ValueError
Output:
['python']
ValueError: add_tag: empty string argument
One decorator, applied to every method that takes string input. Write once, use everywhere.
PRIMM-AI+ Practice: Decorator Tracing
Predict [AI-FREE]
Press Shift+Tab to enter Plan Mode before predicting.
Read this code and predict the exact output. Write your prediction and a confidence score from 1 to 5.
import functools
def count_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
wrapper.call_count += 1
print(f"{func.__name__} called {wrapper.call_count} time(s)")
return func(*args, **kwargs)
wrapper.call_count = 0
return wrapper
@count_calls
def add(a: int, b: int) -> int:
return a + b
result1 = add(2, 3)
result2 = add(10, 20)
print(f"Results: {result1}, {result2}")
print(f"Total calls: {add.call_count}")
Check your prediction
add called 1 time(s)
add called 2 time(s)
Results: 5, 30
Total calls: 2
The decorator adds a call_count attribute to the wrapper function. Each call increments it. Because functools.wraps preserves the name, the print shows add (not wrapper). The count persists across calls because wrapper is a single function object.
Run
Press Shift+Tab to exit Plan Mode.
Create a file called decorator_practice.py. Write the count_calls decorator and add function. Then apply @count_calls to a second function multiply(a, b). Verify that each function tracks its own count independently.
Investigate
Write first: In one sentence, explain why add and multiply each track their own call_count independently. Where does each function's count live?
Then, if you want to go deeper, run /investigate @decorator_practice.py in Claude Code and ask: "Why does each decorated function have its own call_count? Where is the count stored? What would happen if I stored the count as a global variable instead of on wrapper?"
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: Decorator Use Cases
Give me five real-world use cases for custom decorators
in Python applications. For each, show a minimal
implementation (under 10 lines) and explain what
cross-cutting concern it addresses.
What you're learning: Decorators solve cross-cutting concerns: logging, timing, caching, authentication, validation. Seeing multiple use cases makes the pattern feel general, not specific to SmartNotes.
Prompt 2: Decorator for Your Domain
I work in [describe your professional domain]. I have
functions that need [describe a repeated behavior:
audit logging, input validation, retry on failure,
permission checking, etc.].
Write a decorator that handles this cross-cutting concern.
Show it applied to a realistic function from my domain.
What you're learning: You are transferring the decorator pattern to your own context. The AI generates the decorator, but you evaluate whether it correctly wraps the function, uses functools.wraps, and handles the cross-cutting concern appropriately.
James looks at the @validate_input decorator on add_tag and remove_tag. "I wrote the validation once, and it applies to both methods. If I add a third method that takes strings, I add one line: @validate_input."
Emma nods. "Think of it like your quality inspection stamp. Every product gets inspected the same way. You do not write a new inspection process for each product type. You write the process once and stamp it on every line."
"That is exactly what I was thinking," James says.
Emma pauses. "Honestly, I got the stamp analogy from you. I usually explain decorators as wrappers or envelopes, but the stamp is better. It captures the 'applied once, runs every time' behavior. The wrapper analogy makes people think the original function is gone. It is not gone. It is inside the stamp."
"Decorators modify behavior. Properties compute values. Static and class methods organize code. But there is still a problem. I want to test SmartNotes with a fake database instead of a real one. How do I swap the storage without changing the class?"
"That is a Protocol. Next lesson."