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:

Try it — edit and run

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:

Try it — edit and run

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

Try it — edit and run

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.

Try it — edit and run

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:

Try it — edit and run

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.

Try it — edit and run

from_config takes a dict shaped differently than __init__’s two separate arguments, and still produces a normal Agentcls(...) 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:

Try it — edit and run

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.

Check your understanding
1/7

What is @my_decorator above a function definition actually equivalent to?

Exercise · Graded

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.

Exercise · Graded

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.