How do you make a Python decorator remember state, such as a call count, between calls?
answer
- The decorator body runs once
- The wrapper's locals die each call
- State lives in the enclosing scope
- nonlocal, a mutable object, or wrapper.calls
- One counter per decorated function
basics
~20 sPut 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.
solid answer
~50 sA decorator is a function that runs **once**, when the `@` line executes at definition time, and returns a wrapper that runs on every call. Anything you want remembered has to live outside the wrapper's own locals, because those are recreated on each call. Three idioms do it: close over a variable in the decorator body and declare `nonlocal` inside the wrapper before rebinding it; close over a mutable container such as a `dict` or a `list` and mutate it, which needs no `nonlocal`; or set an attribute on the wrapper object itself, for example `wrapper.calls = 0` before returning it. The attribute form is usually the friendliest because callers can read and reset the state as `settle.calls`. Note that one counter exists per decorated function, created at decoration time, and that `calls += 1` is not atomic across threads.
code
python · 19 linesimport functools
def count_calls(func):
calls = 0 # created once, when the decorator runs
@functools.wraps(func)
def wrapper(*args, **kwargs):
nonlocal calls # rebind the enclosing name, do not shadow it
calls += 1
return func(*args, **kwargs)
wrapper.get_calls = lambda: calls
return wrapper
@count_calls
def settle(batch_id):
return batch_id
settle(1)
settle(2)
print(settle.get_calls()) # 2go deeper
Be ready to say that @deco runs once at definition time and that the returned wrapper runs per call, then show one way to keep a counter - a nonlocal variable or wrapper.calls.
Explain why a bare calls += 1 inside the wrapper raises UnboundLocalError, why mutating a dict needs no nonlocal, and why each decorated function gets its own independent state.
Show the production angle: expose and reset the state deliberately, keep anything unbounded clearable, and know that += 1 is not atomic so a shared counter needs a lock or an atomic counter.
Own the choice of where such state belongs at all - an in-process counter inside a decorator is invisible to operators, so decide when it should instead feed a metrics surface with real names, labels and lifetimes.
A decorator is just a callable that takes a function and returns a replacement. `@count_calls` above `def settle(...)` is exactly `settle = count_calls(settle)`, and that assignment happens once, while the module is being executed. The wrapper it returns is what runs on every subsequent call. That two-phase structure is the whole answer to "where does state live": the decorator body is the setup phase that runs once, the wrapper body is the per-call phase, and state has to be created in the first so it can be seen from the second. **Why a plain local does not work.** If you write `calls = 0` inside the wrapper, it is recreated on each call and always reads zero. If you write `calls = 0` in the decorator body and then `calls += 1` in the wrapper, the compiler sees an assignment to `calls` inside the wrapper and classifies it as a *local* of the wrapper, so the read on the right-hand side finds nothing bound yet and Python raises `UnboundLocalError`. The declaration `nonlocal calls` at the top of the wrapper tells the compiler that the name belongs to the enclosing decorator scope, and rebinding then updates the shared value. **Three idioms, in the order you will meet them.** 1. *Closure plus `nonlocal`.* `calls = 0` in the decorator body, `nonlocal calls` then `calls += 1` in the wrapper. Clean, but the value is awkward to read from the outside: the caller only has the wrapper, and the counter is hidden inside it, so you end up exposing an accessor. 2. *Closure over a mutable object.* `stats = {"calls": 0}` in the decorator body, `stats["calls"] += 1` in the wrapper. No `nonlocal` is needed because you are mutating an object rather than rebinding a name. This is the usual shape when the state is a cache dict, a deque of timestamps for a rate limiter, or a set of seen keys. 3. *An attribute on the wrapper.* `wrapper.calls = 0` after defining the wrapper, `wrapper.calls += 1` inside it. Functions are ordinary objects with a `__dict__`, so this works, and it gives the outside world a name to read and reset: `settle.calls`, `settle.calls = 0`. Set the attribute *after* applying `functools.wraps`, because `wraps` copies the wrapped function's `__dict__` over the wrapper's and can overwrite what you set first. **One state object per decorated function.** Because the decorator runs once per `@` line, each decorated function gets its own closure and therefore its own counter. Decorating `settle` and `refund` with the same decorator gives two independent counters; a module-level global would instead give one shared counter and is the classic wrong answer. The flip side is that the state is per *function*, not per call site and not per instance: decorate a method in a class body and every instance of that class shares the one counter, because there is exactly one function object in the class. **Lifetime and reset.** The state lives as long as the wrapper does, which for a module-level function means as long as the process. Nothing evicts it, so a counter is harmless but a growing cache dict is not - give anything unbounded an explicit way to clear it, such as `wrapper.cache = cache` plus a documented `clear()`. **Reading and resetting it.** Whichever idiom you pick, decide deliberately how the state is read back and cleared. An attribute makes both trivial - `settle.calls` reads it, `settle.calls = 0` resets it - while a `nonlocal` variable needs an accessor and a setter you write by hand. This matters most in tests: the state persists for the process, so a test that asserts a count sees whatever earlier tests left behind, and a suite that passes alone but fails in a full run is the classic symptom. Reset the state in setup rather than relying on test order. **Thread safety.** `calls += 1` compiles to a read, an add and a store. Those steps can interleave across threads, so counts can be lost; if the number matters, guard it with a `threading.Lock` or use an inherently atomic operation such as `itertools.count().__next__`. This matters more on the free-threaded build, where no global lock coarsely serializes bytecode. **When to reach for a class instead.** Once the state is more than one counter, or you want methods that inspect and reset it, a class implementing `__init__` and `__call__` reads better than a closure with attributes bolted on: the state becomes plain instance attributes. The tradeoff is that the decorated name is then an instance rather than a function, which changes what introspection and other tooling see.
- Why does the mutable-container form work without a `nonlocal` declaration?`nonlocal` is only needed to *rebind* a name from an enclosing scope. `stats['calls'] += 1` never rebinds `stats`; it looks the name up in the closure and mutates the object it points at. The same applies to appending to a list or inserting into a cache dict, which is why most real stateful decorators hold a dict rather than a counter variable.
- If you decorate a method in a class body with a call-counting decorator, is the count per instance or shared?Shared. Decoration happens once, on the single function object stored in the class, so every instance calling that method increments the same counter. Per-instance counting needs the state keyed by the instance - and holding instances as dict keys keeps them alive, so a `weakref.WeakKeyDictionary` is the usual fix.
- Where should you set `wrapper.calls = 0` relative to `functools.wraps`?After. `functools.wraps` copies the wrapped function's `__dict__` into the wrapper's, so attributes you set before decoration can be clobbered by that update. Applying `wraps` as the decorator on the wrapper definition and then setting your attribute on the finished wrapper object is the safe order.
The decorator body is the factory floor and the wrapper is the product: anything stamped into the machine once, at build time, is still there for every unit that comes off it, while notes scribbled on a single unit vanish with it.
saying these in an interview costs you the question
- Thinks the decorator body runs on every call
- Uses one module-level global for every decorated function
- Expects `count += 1` to update the enclosing name without `nonlocal`
- Believes each call starts with a fresh counter
- Assumes a decorated method counts per instance
- Claims decorators cannot hold state at all