skip to content

Stateful and Class-Based Decorators

When a decorator must remember a call count, a cache or a rate-limit window, you close over a mutable object or write a class with __call__. It is a direct probe of closures and nonlocal.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

3

How does a class implementing __init__ and __call__ work as a Python decorator?

level: middleimportance: must knowfreq 55%

answer

  1. @Cls means name = Cls(func)
  2. Constructor runs once, at decoration
  3. The instance must be made callable
  4. State becomes plain instance attributes
  5. Identity metadata needs copying by hand

basics

~20 s

@Cls above a def calls Cls(func), so __init__ receives the function once at decoration time and stores it plus any state. The decorated name is now an instance; calling it runs __call__, which invokes the stored function.

solid answer

~40 s

Decoration is just `settle = CountCalls(settle)`, so applying a class runs its constructor once with the function as the argument. `__init__` stashes the function on `self` and initialises whatever state you need - a counter, a cache dict, a rate-limit window. Because the resulting object is not a function, it only becomes callable by defining `__call__`, which runs on every call and delegates to `self.func(*args, **kwargs)`. The payoff over a closure is that state is plain instance attributes, so you get natural inspection and reset methods instead of accessors bolted onto a wrapper. The cost is that the decorated name is an instance rather than a function: metadata does not come along unless you call `functools.update_wrapper(self, func)` in `__init__`, and code that expects a real function object may see something different.

code

python · 20 lines
python
import functools

class CountCalls:
    def __init__(self, func):
        functools.update_wrapper(self, func)   # copies __name__, __doc__, sets __wrapped__
        self.func = func
        self.calls = 0

    def __call__(self, *args, **kwargs):
        self.calls += 1
        return self.func(*args, **kwargs)

@CountCalls
def reconcile(batch_id):
    return batch_id

reconcile("2026-09-03")
reconcile("2026-09-04")
print(type(reconcile).__name__, reconcile.calls, reconcile.__name__)
# CountCalls 2 reconcile

go deeper

for a junior

Remember the desugaring: @Cls above a def means name = Cls(func). Know that the class needs __call__ to be callable afterwards, and that __init__ receives the function.

for a middle

Be able to write the class from memory and say which method runs once and which runs per call, then map it onto the closure form: __init__ is the decorator body, __call__ is the wrapper.

for a senior

Show the consequences you have hit in real code: missing identity metadata until functools.update_wrapper, the decorated name no longer being a function, and when the extra structure genuinely beats a closure.

for a principal

Own the guidance for a codebase: when a decorator is the right abstraction at all versus an explicit call, and how much hidden per-function state a team should tolerate before it belongs in a named collaborator.

The `@` syntax has one meaning: `@expr` above `def name(...)` binds `name` to the result of calling `expr` with the function. Nothing requires that `expr` be a function. Any callable works, and a class is callable - calling it constructs an instance. So `@CountCalls` above `def reconcile(...)` is exactly `reconcile = CountCalls(reconcile)`, and afterwards the module-level name `reconcile` refers to a `CountCalls` instance that happens to hold the original function. ## The two methods, and when each runs - **`__init__`** runs exactly once, at decoration time, while the module is being executed. It receives the undecorated function as its argument, and its job is setup: `self.func = func`, plus whatever state the decorator needs. - **`__call__`** is what makes an instance callable at all; without it, `reconcile("b1")` raises `TypeError: 'CountCalls' object is not callable`. `__call__` runs on every call, does the before-work, delegates with `return self.func(*args, **kwargs)`, and does the after-work. That maps one-to-one onto the closure form: - `__init__` corresponds to the decorator body, - `__call__` corresponds to the wrapper, - and instance attributes correspond to closure variables. ## Why anyone bothers State that is more than one counter reads much better as attributes. A rate-limiting decorator holding a window and a limit, a retry decorator holding an attempt budget and a backoff, an audit decorator holding a list of failures - each becomes `self.window`, `self.limit`, `self.attempts`, and you can add real methods: `reset()`, `stats()`, `snapshot()`. Callers reach them naturally, because the decorated name *is* the object: `reconcile.calls`, `reconcile.reset()`. In the closure form the equivalent is a scatter of function attributes and hand-written accessors. ## What you give up, and how to repair it The instance is not a function, and Python does not copy the wrapped function's identity for you. Without help: - `reconcile.__name__` raises `AttributeError` because a plain instance has none, - `reconcile.__doc__` shows the *class's* docstring rather than the function's, - and tooling that reads those attributes reports the decorator instead of the function. The fix is **`functools.update_wrapper(self, func)`** inside `__init__`: it copies `__name__`, `__qualname__`, `__doc__`, `__module__` and `__dict__` onto the instance and sets `__wrapped__` so introspection can follow the chain back. Note that `functools.wraps` is the decorator-shaped convenience for wrapping a *function*; on a class-based decorator you call `update_wrapper` directly on `self`. There are further consequences worth naming honestly. - **Pickling** a decorated name pickles the instance, not the function, and pickle resolves module-level names by import - a class instance under a function's name is a common surprise there. - **Equality and identity** checks against the original function fail. - And **decoration is heavier**: you construct an object per decorated function rather than a closure, though this is once-per-definition and never a real cost. ## Two things to keep straight ### Distinguish it from decorating a class - "Class-based decorator" means a *class used as* a decorator, applied to a function. - "Class decorator" means a function applied to a *class*, receiving and returning the class object - a different mechanism with a different purpose. Interviewers use the phrases loosely; say which one you mean. ### One instance per decorated function As with the closure form, decoration happens once per `@` line, so two decorated functions get two independent instances holding independent state. The bug to avoid is putting the counter or cache on the *class* instead of on `self`: a class attribute is shared by every instance, so every decorated function in the module would then increment one number, which is exactly the shared-global mistake in fancier clothing. ## Subclassing is the real payoff Because the behaviour lives in `__call__`, a base class can own the delegation skeleton - argument forwarding, metadata copying, error handling - and subclasses override a single hook: one counts, one times, one records failures. Reuse like that is awkward with closures, where sharing behaviour means threading extra functions through the factory. ## Choosing between the forms - **Prefer a closure** when the decorator is small and stateless, or holds a single counter or dict - it is less code and the result is still a function object. - **Prefer a class** when the state is structured, when you want to expose operations on it, or when subclassing gives you a family of related decorators sharing a `__call__` skeleton. - **Prefer neither over the stdlib** when the stdlib already has it: reach for a ready-made tool before hand-rolling one with hidden state. ## A last practical detail Because `__init__` takes the function positionally, a class-based decorator applied bare (`@CountCalls`) and one applied with arguments (`@CountCalls(limit=5)`) are different shapes. The second means `CountCalls(limit=5)` returns an instance whose `__call__` receives the *function* and returns the real wrapper - which is the decorator-factory pattern, a different construction from the one described here.

  • After `@CountCalls` is applied, what is the type of the decorated name, and why does that matter?
    It is a `CountCalls` instance, not a function. That matters for anything that inspects the object: `__name__` and `__doc__` are missing or wrong unless you call `functools.update_wrapper(self, func)`, pickling by name yields the instance, and identity comparisons against the original function fail. The upside is that the state is reachable as ordinary attributes on that instance.
  • Why does `functools.wraps` not fit a class-based decorator the way it fits a nested wrapper function?
    `functools.wraps` is a decorator factory meant to be applied to a wrapper *function* definition. In a class-based decorator there is no separate wrapper function to decorate - the instance itself is the callable - so you call the underlying `functools.update_wrapper(self, func)` inside `__init__` instead, which copies the same attributes onto the instance and sets `__wrapped__`.
  • When would you still choose a closure over a class-based decorator?
    When the decorator is stateless or holds one counter or dict, and when the result needs to stay a real function object - the closure form is shorter and preserves function-ness. A class earns its keep once the state is structured or you want to expose methods such as `reset()` and `stats()` on the decorated name.

saying these in an interview costs you the question

  • Thinks `__init__` runs on every call of the decorated function
  • Omits `__call__` and expects the instance to be callable
  • Believes the decorated name is still a plain function
  • Confuses a class used as a decorator with decorating a class
  • Skips `functools.update_wrapper` then blames the missing `__name__`
  • Stores the function in a class attribute shared by all instances

context

open as a page

How do you make a Python decorator remember state, such as a call count, between calls?

level: juniorimportance: should knowfreq 50%

basics

~20 s

Put the state in the decorator's own scope, outside the wrapper: a variable rebound with nonlocal, a mutable object such as a dict, or an attribute set on the wrapper function. The decorator body runs once, so that state survives every call.

open as a page

What goes wrong with a hand-rolled decorator that caches results in a plain dict?

level: seniorimportance: should knowfreq 45%

basics

~20 s

The dict is created once at decoration time and never evicts, so it grows for the life of the process, pins whatever it holds, caches failures and stale values, breaks on unhashable arguments, and races under concurrent threads.

open as a page