A wrapper needs somewhere to keep what it learned. Read why the value goes in a dictionary rather than a plain variable.
def counted(function):
state = {'calls': 0}
def wrapper(*args, **kwargs):
state['calls'] += 1 # mutating the dict is allowed
# calls += 1 # this would be UnboundLocalError
return function(*args, **kwargs)
wrapper.calls = lambda: state['calls']
return wrapper
- Assigning to a name makes it local
Writing calls += 1 inside wrapper would make calls a local variable of wrapper, and reading it before assignment raises UnboundLocalError. Mutating a dictionary is not assignment to the name, so the outer state is reached happily. `nonlocal calls` is the other fix, and the dictionary is the one you will see more often in the wild.
- *args, **kwargs passes anything through
The wrapper does not know or care what signature the wrapped function has. Collecting everything and forwarding it unchanged is what lets one wrapper work for every function — and it is why decorators can be applied to anything.
- The state lives as long as the wrapper does
state is created once, when counted runs, and the closure keeps it alive for every later call. That is the whole mechanism behind caches, counters and run-once guards — no globals, and one independent store per wrapped function.
The wrapper has to remember two things between calls: whether it has run, and what came back. Think about how to store both without a flag that a real result could imitate.
Keep a dictionary in the enclosing scope. Presence of a key answers 'have I run?' — which works even when the stored value is None, False or zero, because you are asking about the key rather than the value.
state = {}. Inside the wrapper: if 'result' not in state, call the function with *args and **kwargs and store the answer under that key. Then return state['result']. Return the wrapper without brackets.
def once(function): state = {} def wrapper(*args, **kwargs): if 'result' ____ ____ state: state['result'] = function(*args, **kwargs) return state[____] return ____
def once(function):
state = {}
def wrapper(*args, **kwargs):
if "result" not in state:
state["result"] = function(*args, **kwargs)
return state["result"]
return wrapper
`'result' not in state` is true exactly once, whatever the function returned. The tempting alternatives — a `result = None` variable, or a `has_run = False` flag — either break on a None result or add a second thing to keep in step with the first. One dictionary answers both questions and cannot disagree with itself.
nonlocal has_run, result works and needs two names updated together in one place. That is fine here and stops being fine as soon as a third piece of state appears. The dictionary scales without changing shape, which is why you see it in library code.
Nothing is stored when the call raises, so a later call tries again. That is defensible — the setup did not happen, so retrying is reasonable — and so is the opposite, caching the failure so a broken configuration fails fast every time. The contract says nothing, which means a real implementation should say something.
If the function takes arguments that genuinely vary, you want memoisation keyed by those arguments, not this — functools.lru_cache does that properly and is written in C. If the point is one-per-process setup, a module-level variable is often clearer than a wrapper, because import already guarantees the module body runs once. And none of this is thread-safe: two threads arriving together can both see an empty state and both run the function, which for a connection pool is exactly the bug you were trying to prevent. Reach for a lock, or for importlib’s guarantees.
- Cache the exception too, so a failed first call fails identically forever — then argue which behaviour you would want for reading a config file.
- Add a .reset() on the wrapper so tests can clear the memory, and notice how much easier the function becomes to test.
- Make it thread-safe with a lock, then work out why checking the state twice — before and after taking the lock — is the usual pattern.