Python Nonlocal Closure: Modifying Captured Variables
Understand how the python nonlocal closure pattern lets nested functions reassign captured variables, with practical examples and common failure modes.
The python nonlocal closure pattern solves a specific problem: a nested function that needs to reassign a variable bound in its enclosing function's scope. Reading a captured variable works without any special syntax. Reassigning it does not. Consider a nested function that tries to update a counter defined in its parent:
def make_counter(): count = 0 def increment(): count += 1 return count return increment
Calling increment() raises UnboundLocalError: local variable 'count' referenced before assignment. The reason is that count += 1 is an assignment, and Python's scoping rules treat any assigned name in a function as local to that function unless you explicitly say otherwise. The inner function therefore creates a new local count that shadows the outer one, and the += operation reads it before it has been assigned.
The nonlocal statement resolves this. It declares that a name refers to a variable bound in the nearest enclosing function scope, not the current one and not the module scope. With nonlocal count in place, the inner function modifies the same count object that make_counter created.
How nonlocal Changes Variable Binding
Python resolves names at compile time, not at runtime. When the interpreter compiles a function, it classifies every name it sees. A name that appears on the left side of an assignment is local to that function. A name declared global is looked up in the module namespace. A name declared nonlocal is looked up in the nearest enclosing function scope that binds that name.
The nonlocal declaration must appear at the top of the function body, before any use of the name. It can only refer to a name that exists in an enclosing function scope. If no enclosing function binds the name, Python raises SyntaxError: no binding for nonlocal 'x' found. It also cannot refer to a module-level name; that is what global is for.
A nested function can declare multiple names in one nonlocal statement, separated by commas:
def outer(): total = 0 calls = 0 def inner(): nonlocal total, calls total += 1 calls += 1 return inner
Each name must have a binding in some enclosing function scope. The binding can be several levels up, not just in the immediate parent.
A Minimal Counter Example
The canonical use of nonlocal is a counter that persists state between calls. Here is the corrected version of the earlier example:
def make_counter(): count = 0 def increment(): nonlocal count count += 1 return count return increment counter = make_counter() print(counter()) # 1 print(counter()) # 2 print(counter()) # 3
Each call to counter() reads the current value of count, increments it, and stores the new value back into the same variable. The variable lives in the closure's cell, which persists as long as the returned function object exists. If you create a second counter, it gets its own independent count:
counter_a = make_counter() counter_b = make_counter() counter_a() # 1 counter_a() # 2 counter_b() # 1
The two closures do not share state because each call to make_counter creates a fresh count binding.
Why Assignment Fails Without nonlocal
The failure mode is subtle because reading a captured variable works fine. This code runs without error:
def make_reader(): value = 42 def read(): return value return read
The inner function only reads value, so Python treats it as a free variable resolved from the enclosing scope. The moment you add an assignment, the classification changes. This is why the error message says the variable is referenced before assignment rather than saying it does not exist. Python has already decided the name is local to the inner function, so the += reads a local that has not been initialized.
The same problem occurs with any augmented assignment operator, including -=, *=, and |=, and with method calls that rebind the name, such as items = items + [1]. Mutating an object in place, such as items.append(1), does not require nonlocal because the name items is never reassigned. The distinction is between rebinding a name and mutating the object it points to.
nonlocal vs global in Nested Functions
Both statements change how Python resolves a name, but they target different scopes. global binds a name to the module's namespace. nonlocal binds a name to the nearest enclosing function scope. A nested function can use both in the same body, though mixing them is rare.
| Statement | Scope resolved | Requirement |
|---|---|---|
| (none) | Local to current function | Name assigned in body |
global | Module namespace | Name may exist at module level |
nonlocal | Nearest enclosing function scope | Name must exist in an enclosing function |
A common mistake is using global inside a nested function when the intent is to modify a variable in the outer function. That works only if the variable also exists at module level, and it silently changes the wrong binding if it does. nonlocal is the correct choice whenever the variable belongs to an enclosing function.
Practical Use: Stateful Decorators
Decorators often need to track state across calls. A call counter is a straightforward example:
def count_calls(func): calls = 0 def wrapper(*args, **kwargs): nonlocal calls calls += 1 print(f"{func.__name__} called {calls} times") return func(*args, **kwargs) return wrapper
The calls variable lives in the closure created by count_calls. Each decorated function gets its own counter because each call to count_calls creates a fresh binding. Without nonlocal, the wrapper would raise UnboundLocalError on the first invocation.
A cache with a bounded size follows the same pattern. The cache dictionary itself can be mutated without nonlocal, but a counter that tracks cache hits must be rebound on each update and therefore needs the declaration.
Runtime Behavior and Maintainability
The state that nonlocal exposes lives in a closure cell, which is a small heap-allocated object. The cell persists as long as the inner function object is referenced. This has two practical consequences. First, the outer function's frame is released when it returns, but the variables captured by the closure remain alive in the cells. Second, if the inner function is stored in a long-lived structure, the captured variables stay in memory for the same lifetime.
This persistence is usually what you want, but it can surprise developers who expect the outer function's state to disappear. A closure that captures large data structures keeps them alive, so be deliberate about what the inner function captures. If a nested function only needs a value once, passing it as a default argument or returning a small object may be simpler than maintaining closure state.
For maintainability, keep nonlocal usage confined to a small number of variables and document what each one represents. A closure with several nonlocal names becomes hard to reason about because the state is implicit. If you find yourself declaring many nonlocal names, consider whether a small class or a dataclass would express the state more clearly. The closure approach is concise and works well for one or two variables; beyond that, an explicit state object is often easier to test and debug.