Decorators, @staticmethod and @classmethod
What a decorator actually is — a function wrapping a function
Before the @ syntax, it helps to see what’s actually happening
underneath it. A decorator is just a function that takes another function
as input and returns a new function — nothing more exotic than that:
shout takes greet in, defines a new function wrapper that calls the
original and modifies its result, and returns wrapper. Reassigning
greet = shout(greet) replaces the original with the wrapped version —
every future call to greet(...) actually runs wrapper(...).
The @ syntax is exactly this pattern, just without writing the
reassignment yourself:
@shout directly above def greet(...) is exactly equivalent to
greet = shout(greet) — Python does that reassignment automatically, the
moment greet is defined. Nothing about decorators requires the @
syntax; it’s sugar over a pattern you could write by hand.
Writing a decorator that preserves arbitrary arguments
wrapper(*args, **kwargs) above isn’t incidental — it’s what makes a
decorator reusable across any function, regardless of its signature,
using exactly the
forwarding pattern from earlier in the course:
collect whatever was passed in, forward it through to the wrapped function
untouched.
One @log_call decorator works transparently on both a two-positional-arg
function and a function with a keyword argument, without being rewritten
for either — this is the same generality *args/**kwargs gave
logged_call as a plain wrapper function; a decorator is that same idea,
just applied via @ instead of a manual reassignment.
@staticmethod — a method that needs neither self nor the class
Every method you’ve written so far takes self. Sometimes a function
genuinely belongs inside a class — conceptually grouped with it — but
doesn’t need to read or modify any instance or class state at all. That’s
what @staticmethod is for: it strips self out of the method entirely.
is_valid_name doesn’t touch self anywhere — it’s just a validation
function that makes sense grouped with Agent rather than floating
free at module level. Note it’s called as Agent.is_valid_name(...), with
no instance required at all. Without @staticmethod, this breaks:
Without @staticmethod, Python still treats this as a normal instance
method expecting self as its first parameter. Called directly on the
class (no instance), there’s nothing to fill self with — so the single
string you passed gets bound to self, leaving name with no value at
all. @staticmethod is what tells Python “don’t do the automatic self
binding for this one.”
@classmethod — a method that needs the class, not an instance
A classmethod takes cls instead of self — it receives the class
itself, not a specific instance. The most common real use: an alternate
constructor, building an instance a different way than __init__
expects.
from_config takes a dict shaped differently than __init__’s two
separate arguments, and still produces a normal Agent — cls(...)
inside a classmethod calls the class’s own __init__, exactly like
writing Agent(...) would, except it works even if this code were
inherited by a subclass (covered later this lesson), where cls would
correctly refer to the subclass instead of being hardcoded to Agent.
@classmethod is also the cleaner, more explicit place to manipulate
class-level state, compared to reaching for ClassName.variable inside a
regular instance method — recall
Agent.total_created from earlier in this lesson:
reset_count needs the class (to reset its shared counter) but no
particular instance — exactly the situation @classmethod is for. Inside
it, cls.total_created and Agent.total_created do the same thing here,
but cls is the better habit: it stays correct even if a subclass calls
reset_count() on itself later.
Choosing between the three
One class, side by side, makes the decision rule concrete:
class ToolCall:
total_calls = 0
def __init__(self, tool_name: str, result: str):
self.tool_name = tool_name # needs self — per-instance data
self.result = result
ToolCall.total_calls += 1
def summary(self) -> str: # needs self — reads instance data
return f"{self.tool_name} -> {self.result}"
@classmethod
def from_raw_response(cls, response: dict): # needs the class, not an instance
return cls(response["tool"], response["output"])
@staticmethod
def is_valid_tool_name(name: str) -> bool: # needs neither
return len(name) > 0(not run live — illustrating the pattern side by side, not a new output)
The rule: does it need to read or modify this specific instance’s data
(self.name, self.result)? Regular instance method. Does it need the
class — building an instance a different way, or touching shared class
state — but not any one instance? @classmethod. Does it need neither, and
is just grouped with the class for organization? @staticmethod.
What is @my_decorator above a function definition actually equivalent to?
Write count_calls as a decorator. Its inner wrapper should forward any arguments to func with *args/**kwargs, increment a wrapper.calls counter each time it's called, and return func's actual result unchanged.
Implement __init__ and a class variable total_calls as before, then add from_raw_response as a @classmethod that builds an instance from a differently-shaped dict using cls(...), and is_valid_tool_name as a @staticmethod that validates a name without needing any instance or class state.