Closures and Overwriting Variables in Python 2026
When working with closures, reassigning (overwriting) a nonlocal variable inside the inner function can lead to unexpected behavior. Understanding how Python handles variable binding in closures is essential to avoid common bugs.
TL;DR — Key Takeaways 2026
- Reassigning a nonlocal variable inside a closure requires the
nonlocaldeclaration - Without
nonlocal, Python treats the assignment as a new local variable - Overwriting variables in closures can break the intended shared state
- Always use
nonlocalwhen you intend to modify the enclosing variable
1. The Common Mistake
def make_counter():
count = 0
def counter():
# count += 1 # This would raise UnboundLocalError!
nonlocal count # Correct way
count += 1
return count
return counter
c = make_counter()
print(c()) # 1
print(c()) # 2
2. Overwriting vs Modifying
def create_logger(name: str):
log_level = "INFO"
def log(message: str, level: str = None):
nonlocal log_level
if level:
log_level = level # Overwriting the variable
print(f"[{log_level}] {name}: {message}")
return log
logger = create_logger("App")
logger("Starting application") # [INFO] App: Starting application
logger("Something went wrong", level="ERROR") # [ERROR] App: Something went wrong
logger("Normal message") # [ERROR] App: Normal message
3. Best Practices in 2026
- Always use
nonlocalwhen you want to reassign or modify a variable from the enclosing scope - Avoid overwriting nonlocal variables unless the change is intentional and well-documented
- Prefer immutable data or returning new values instead of mutating shared state in closures
- Use classes instead of complex closures when state management becomes complicated
- Clearly document any variable overwriting behavior in the closure
Late Binding in Closures
Closures look up captured variables when they are called, not when they are created. This is called late binding. If the outer variable is overwritten before the call, the closure sees the new value.
The classic surprise is creating functions in a loop. Every lambda refers to the same loop variable, so after the loop they all see its last value. The fix is to bind the current value when each function is created, either with a default argument or with functools.partial.
Late binding is also useful: a closure can read a configuration value that changes over time. The key is to be deliberate about which behaviour you want.
Writing a short test that calls each function from a loop catches this bug immediately.
Worked Example: The Loop Bug and Two Fixes
We create multipliers in a loop, first with the bug and then with two fixes.
from functools import partial
late = [lambda x: x * n for n in range(1, 4)]
print([f(10) for f in late])
bound = [lambda x, n=n: x * n for n in range(1, 4)]
print([f(10) for f in bound])
def multiply(x, n):
return x * n
parts = [partial(multiply, n=n) for n in range(1, 4)]
print([f(10) for f in parts])
rate = 0.1
fee = lambda amount: amount * rate
rate = 0.2
print(fee(100))
The buggy list prints [30, 30, 30], because each lambda sees the final n of 3. The default-argument and partial versions print [10, 20, 30]. The fee example prints 20.0, showing late binding picking up the updated rate.
Practical Tips
When building handlers in a loop, add a quick check right after the loop that calls each handler once and prints the result. Wrong values show up immediately.
Prefer functools.partial over default arguments when the bound value should not be overridable by callers, because partial keeps the parameter list clean.
Common Mistakes
- Creating callbacks in a loop without binding the loop value.
- Exposing bound defaults as parameters callers might override.
- Relying on late binding without documenting it.
- Testing only the first function created in a loop.
Quick Summary
- Closures read variables at call time.
- Loops create the classic late-binding bug.
- Bind values with defaults or partial.
- Test every function built in a loop.
Conclusion
Closures and variable overwriting can be tricky if not handled correctly. In 2026, the rule is simple: always declare nonlocal when you intend to reassign a variable from the enclosing scope. For complex state, classes usually provide clearer and more maintainable solutions than deep closures with heavy overwriting.
Next steps:
- Review your closures and ensure every reassignment of enclosing variables uses the
nonlocalkeyword