Python Closures and `nonlocal`: How Functions Retain Enclosing State
A closure captures the binding, not the value. That distinction explains the loop bug, the counter that refuses to count, and why your factory returns…

Key topics
A closure captures the binding, not the value. That distinction explains the loop bug, the counter that refuses to count, and why your factory returns three functions that all agree on the wrong answer.
Here is the failure I see most often in code review. Someone builds a list of handlers in a loop, returns them, and every handler reports the last value:
def build_handlers():
handlers = []
for level in ["debug", "info", "error"]:
handlers.append(lambda msg: f"[{level}] {msg}")
return handlers
for handler in build_handlers():
print(handler("disk almost full"))
[error] disk almost full
[error] disk almost full
[error] disk almost full
Three functions. One answer. The natural diagnosis is that the closure "froze" the value of level when the lambda was defined, and something went wrong with the freezing. That diagnosis is wrong, and it will keep you wrong until you replace it.
The Bug That Looks Like a Copy Problem
The default belief is that a closure snapshots its environment at definition time. Under that model, each lambda should have captured "debug", "info", and "error" respectively, and the output should differ. Since the output does not differ, the model must be broken somewhere.
It is not broken. It was never true.
There is a narrow case where the snapshot belief produces correct predictions: a factory that only reads its argument and never rebinds it. Consider:
def make_adder(x):
def add(y):
return x + y
return add
add_five = make_adder(5)
print(add_five(10))
15
This works, and it works for a reason that has nothing to do with copying. x is a name bound to 5, the inner function references that name, and nothing ever rebinds it. Read-only capture makes the distinction between "the binding" and "the value" invisible. The moment you rebind, the distinction becomes the entire story.
The replacement model has two parts, and you need both:
- A closure retains access to an enclosing binding — a named location that stays alive as long as any closure still points at it.
- Each call reads the current object held by that binding. If the name was rebound between calls, the closure sees the new object. If the object was mutated in place, the closure sees the same object with new contents.
Not the object. Not a copy. The binding, read live.
The observable consequence is that the same code behaves differently depending on when you call it. Inside build_handlers, during the loop, each lambda would report the current level. After the function returns, all three report the final level, because the loop finished before any of them ran. The functions did not change. The binding they share changed underneath them.
Knowledge check
Check your understanding
Answer this question before you continue.
Names, Cells, and What Actually Gets Captured
A Python name is a binding to an object, not a box holding a value. Rebinding a name and mutating an object are different operations, and closures care about the first one.
When a nested function references a name from an enclosing function scope, the compiler marks that name as a cell variable. The function object carries a reference to the cell, not to the object's current value. The cell is a small indirection layer: a named slot that holds a pointer to whatever object the name currently refers to.
This is why the closure keeps working after the outer function returns. The enclosing frame is destroyed, but the cell is not, because the function object still references it. The cell keeps the object alive. The frame was scaffolding; the cell is the load-bearing structure.
You can inspect this directly. Every function that captures enclosing-scope names exposes __closure__, a tuple of cell objects, and each cell exposes cell_contents:
def counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
c = counter()
print(c.__closure__)
print(c.__closure__[0].cell_contents)
c()
print(c.__closure__[0].cell_contents)
(<cell at 0x...: int object at 0x...>,)
0
1
The cell is the same object across calls. Only its contents change.
Now prove the loop claim with the same tool. Build two handlers in a loop and compare their cells:
def build_two():
fns = []
for level in ["debug", "info"]:
fns.append(lambda msg: f"[{level}] {msg}")
return fns
a, b = build_two()
print(a.__closure__[0] is b.__closure__[0])
print(a.__closure__[0].cell_contents)
True
error
The two handlers share one cell, and that cell holds the final loop value. That is the whole bug in two lines of output. The invariant to carry forward: a shared cell means a shared binding. If the object inside that binding is also mutable, you get a second layer of sharing on top — two closures can observe the same list and both see each other's appends.
One boundary worth drawing: module-level globals are not stored in __closure__. They are looked up dynamically at call time through the module namespace. A function that reads a global is not carrying a cell for it. This matters because globals and captured names fail in similar-looking ways but for different reasons.
A note on the inspection itself: __closure__ is a tuple ordered by the compiler's free-variable list, not by the order names appear in your source. When a function captures several names, index 0 is not guaranteed to be the one you care about. For a single captured name it is unambiguous; for more, print the whole tuple or match cells by identity rather than assuming an index.
Knowledge check
Check your understanding
Answer this question before you continue.
Reading and Rebinding: Why nonlocal Exists
Assignment inside a function makes a name local to that function unless declared otherwise. This is the same rule that produces UnboundLocalError when you read a name before assigning it in the same scope.
So this fails:
def counter():
count = 0
def increment():
count += 1
return count
return increment
c = counter()
c()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in increment
UnboundLocalError: cannot access local variable 'count' where it is not associated with a value
The compiler saw count += 1 inside increment, decided count is local to increment, and then the read half of += hit an unassigned local. The enclosing count was never consulted.
nonlocal is the declaration that resolves this. It binds the name to the nearest enclosing function scope that already defines it:
def counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
nonlocal does not reach module globals. global is a separate declaration for a separate scope. If you write nonlocal and no enclosing function defines the name, you get a SyntaxError at compile time, which is the interpreter telling you the scope you named does not exist.
The asymmetry that confuses people: reading a captured name requires no declaration at all. Only rebinding does. The lambda in build_handlers reads level and never rebinds it, so it needs nothing. The counter rebinds count, so it needs nonlocal.
Knowledge check
Check your understanding
Answer this question before you continue.
Tracing a Closure Across Calls
Here is the corrected counter traced across three calls. The cell contents column is what c.__closure__[0].cell_contents reports.
| Call | Cell before | Cell after | Return |
|---|---|---|---|
| 1 | 0 | 1 | 1 |
| 2 | 1 | 2 | 2 |
| 3 | 2 | 3 | 3 |
Now the same trace for a closure that captures a mutable object instead of rebinding a name:
def cumulative_average():
data = []
def average(value):
data.append(value)
return sum(data) / len(data)
return average
No nonlocal needed. data is never rebound; the list object is mutated in place. The cell still points at the same list across every call. This is the distinction between rebinding a name and mutating an object, and it is why some closures need nonlocal and some do not.
The diagnostic drill I use when a closure misbehaves: print __closure__ and cell_contents at definition time and after each call. If two closures share a cell, they share a binding. That single check converts "why is this wrong" into "which names are aliased."
The loop case is the same mechanism. There is one cell per loop variable, not one per iteration. Every function built in the loop references that one cell, and by the time you call any of them, the loop has finished and the cell holds the final value.
The fix is to bind the value per iteration, either with a default argument or a factory call:
handlers = [lambda msg, level=level: f"[{level}] {msg}" for level in ["debug", "info", "error"]]
The default argument is evaluated at definition time, so each lambda gets its own binding. The cost is that you have changed what is captured: from a variable to a value. That is exactly what you wanted here, and exactly what you would not want if the closure needed to observe later changes.
Knowledge check
Check your understanding
Answer this question before you continue.
Closures vs. Classes: Choosing on Purpose
The decision axis is not "which is more Pythonic." It is how much state you have, how many operations touch it, and whether that state needs to be inspected, serialized, or shared.
A closure wins when there is one operation and a small amount of private state. A configured callback, a rate limiter, a memoized wrapper — these are one function with a little memory attached. A class here is ceremony.
A class wins when state grows, when multiple methods act on the same state, when you need inheritance, or when you need to inspect the state during debugging. The real cost of closures is that the captured state is invisible in the object's public surface. Reading self.count in a debugger is trivial. Reading func.__closure__[0].cell_contents is not something you want to do at 2 a.m.
These are two spellings of the same idea. A class with __call__ is a callable that carries state; a closure is a callable that carries state. The class makes the state visible and extensible. The closure makes it private and compact.
One typing-adjacent note: a closure's captured state is invisible to a type checker. The checker sees the function signature and nothing about the cell behind it.
Where Closures Earn Their Keep — and Where They Don't
Function factories are the canonical case. Pre-bind configuration so callers get a one-argument function instead of a five-argument one:
def make_root_calculator(degree, precision=2):
def root(number):
return round(number ** (1 / degree), precision)
return root
square_root = make_root_calculator(2, 4)
Callbacks and event handlers are the second case: capture the context a handler needs without threading it through every call site. Decorators are the third, and they are not a special case — the wrapper closes over the original function using the same mechanism.
Encapsulation without a class is genuinely useful for small, single-purpose objects. Private state that no external code can reach has real value when the object is one function with one job.
Where closures stop earning their keep: state that must be shared across threads without a lock, state that must be serialized or persisted, state that needs more than a couple of operations, or anything a teammate will need to inspect in a debugger. In those cases, promote the state to a class and stop fighting the invisibility.
One failure mode is worth separating from the shared-cell trap because it looks identical but is not. A mutable default argument — def f(x, cache={}) — shares one dict across every call. That is not a closure cell. The default is evaluated once at function definition and stored in the function's __defaults__; every call reads the same object from there. The mechanism is different, the symptom is the same, and the fix is different: use None as the sentinel and build the dict inside the body. Keep the two in separate mental buckets or you will reach for nonlocal when you needed a sentinel.
My rule: if the state is small, private, and touched by one operation, a closure is the lighter tool. The moment you need to inspect, share, or extend that state, promote it to a class.
Take a callback or factory in your own codebase, print __closure__ and cell_contents before and after a call, and confirm which names are shared cells rather than private copies. Then find one loop that builds callables and check whether the handlers share a cell. That single inspection turns the mental model from something you read into something you can debug.
Knowledge check
Final check
Finish the article by checking the ideas you just learned.
References
Research updated Sep 11, 2026


