Attaching Nonlocal Variables to Nested Functions in Python 2026
Attaching nonlocal variables to nested functions (creating closures) is a powerful technique in Python. It allows inner functions to "remember" and modify variables from their enclosing scope even after the outer function has finished executing.
TL;DR — Key Takeaways 2026
- Use
nonlocalto modify variables from the enclosing scope - The inner function "closes over" the nonlocal variables, creating a closure
- This pattern is widely used for factory functions, decorators, and stateful callbacks
- Keep closures simple — for complex state, prefer classes
1. Basic Closure with nonlocal
def make_counter():
count = 0
def counter():
nonlocal count
count += 1
return count
return counter
# Each call creates its own independent closure
c1 = make_counter()
c2 = make_counter()
print(c1()) # 1
print(c1()) # 2
print(c2()) # 1
print(c1()) # 3
2. Advanced Example – Configurable Factory
def make_formatter(prefix: str, suffix: str = ""):
"""Returns a customized formatting function."""
def formatter(text: str) -> str:
nonlocal prefix, suffix # Allows modification if needed
return f"{prefix}{text}{suffix}"
return formatter
# Create specialized formatters
error_msg = make_formatter("[ERROR] ", "!")
warning_msg = make_formatter("[WARNING] ")
print(error_msg("File not found")) # [ERROR] File not found!
print(warning_msg("Low disk space")) # [WARNING] Low disk space
3. Best Practices in 2026
- Use
nonlocalexplicitly when modifying enclosing variables - Keep the outer function focused and the inner function small
- Document what variables the closure depends on
- Use type hints with
Callablefor the returned function - When state becomes complex, switch to a class instead of a closure
Where Closure Variables Live
When an inner function uses a variable from its enclosing function, Python stores that variable in a cell. The inner function keeps a reference to the cell in its __closure__ attribute, and the variable names are listed in __code__.co_freevars. This is how a returned function still sees variables after the outer function has finished.
Each call of the outer function creates new cells, so every returned function has its own state. With nonlocal, the inner function can update that state between calls.
Inspecting cells is mainly useful for learning and debugging, but it makes clear that closures hold variables, not copies of values.
Because a closure keeps its cells alive, large objects captured by a closure stay in memory as long as the function exists.
Worked Example: Two Accumulators With Separate Cells
We create two accumulators and inspect their closure cells.
def make_accumulator(start=0):
total = start
def add(amount):
nonlocal total
total += amount
return total
return add
sales = make_accumulator()
costs = make_accumulator(100)
sales(50); sales(25); costs(10)
print(sales.__code__.co_freevars)
print(sales.__closure__[0].cell_contents, costs.__closure__[0].cell_contents)
print(sales.__closure__[0] is costs.__closure__[0])
Both functions have one free variable named total. The sales cell holds 75 and the costs cell holds 110, and the cells are different objects, so the two accumulators never interfere.
Practical Tips
Use closures for small amounts of state tied to one behaviour, such as a running total, a counter or a cache. For several related values or methods, a class is clearer.
When debugging a closure, printing f.__code__.co_freevars together with the cell contents shows exactly which values the function remembers. This is a quick way to spot a closure that captured the wrong variable.
Avoid capturing large objects in closures that live for a long time, such as callbacks registered with a framework.
Give factory functions names that start with make_ or build_ so readers know they return a configured function.
Closures created inside loops share the loop variable unless you bind each value with a default argument or functools.partial. Check this whenever you build several closures at once.
For simple counters, itertools.count is a ready-made alternative.
Common Mistakes
- Thinking a closure copies the value at creation time.
- Capturing large DataFrames in long-lived closures.
- Expecting two factory results to share state.
- Relying on __closure__ in production code.
Quick Summary
- Closure variables live in cells.
- __closure__ and co_freevars show them.
- Each factory call creates new cells.
- Captured objects stay alive with the function.
Conclusion
Attaching nonlocal variables to nested functions is a clean and efficient way to create stateful, reusable functions in Python. In 2026, this closure pattern remains one of the most elegant ways to implement factory functions, decorators, and lightweight state management.
Next steps:
- Identify repeated logic in your code and replace it with closure-based factories