How does a class implementing __init__ and __call__ work as a Python decorator?
answer
- @Cls means name = Cls(func)
- Constructor runs once, at decoration
- The instance must be made callable
- State becomes plain instance attributes
- 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 sDecoration 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 linesimport 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 reconcilego deeper
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.
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.
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.
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