The nonlocal keyword in Python allows a nested (inner) function to reference and modify a variable from an enclosing (outer) function’s scope — without treating it as local or global. Introduced in Python 3, nonlocal solves the limitation of nested functions: by default, assignment creates a local variable, shadowing the outer one, and reading without assignment is allowed but modification raises UnboundLocalError. nonlocal declares that the name refers to a variable in the nearest enclosing scope (not global). In 2026, nonlocal remains essential for closures with state (counters, accumulators, factories), decorators with mutable state, and clean nested logic in data pipelines, ML workflows, and GUI/async code — but like global, overuse signals poor design; prefer parameters, return values, or classes for shared state.
Here’s a complete, practical guide to the nonlocal keyword in Python: how it works, reading vs writing, common pitfalls, alternatives to nonlocal, real-world patterns, and modern best practices with type hints, closures, immutability, and pandas/Polars integration.
Basic usage — nonlocal lets inner function modify outer function’s variable.
def outer_function():
x = 1
def inner_function():
nonlocal x # declare x refers to outer scope
x += 1
print("Inner:", x)
inner_function()
print("Outer:", x)
outer_function()
# Inner: 2
# Outer: 2
Reading outer variables without nonlocal is allowed — only assignment requires declaration.
def outer():
count = 0
def inner():
print(count) # reads outer ? 0 (no nonlocal needed)
nonlocal count
count += 1 # modifies outer
inner()
print(count) # 1
outer()
Classic pitfall — forgetting nonlocal when assigning causes UnboundLocalError.
def outer():
x = 10
def inner():
print(x) # UnboundLocalError (local x referenced before assignment)
x = 20
inner()
# Fix
def fixed_outer():
x = 10
def inner():
nonlocal x
print(x) # 10
x = 20
inner()
print(x) # 20
Real-world pattern: stateful closures in pandas/Polars pipelines — use nonlocal for accumulators, counters, or shared config in nested functions.
def rolling_average(window: int):
total = 0
count = 0
def update(value: float) -> float:
nonlocal total, count
total += value
count += 1
if count > window:
# simple rolling: subtract oldest (assumes FIFO)
total -= values[count - window - 1] # assume values list exists
return total / min(count, window)
return update
avg = rolling_average(3)
print(avg(10)) # 10.0
print(avg(20)) # 15.0
print(avg(30)) # 20.0
Best practices make nonlocal safe, readable, and avoidable. Use nonlocal sparingly — only for closures with mutable state (counters, accumulators). Prefer parameters and return values — pass state explicitly for pure functions. Modern tip: use Polars for immutable-by-default DataFrames — most operations return new frames, reducing need for mutable shared state. Add type hints — def outer() -> Callable[[float], float] — improves clarity and mypy checks. Use classes instead of closures for complex state — clearer, easier to test/mock. Avoid deep nesting — extract inner functions to top-level if logic grows. Use nonlocal only in nested functions — never in top-level (use global there). Test closures — assert state changes correctly across calls. Combine with functools.lru_cache — memoize returned functions for performance. Refactor to immutable patterns — prefer new objects over mutation (e.g., Polars chaining). Use contextvars for per-context state in async code — better than globals/nonlocal in threads/async.
The nonlocal keyword lets nested functions modify enclosing scope variables — essential for closures with state. In 2026, use it sparingly, prefer parameters/returns/classes, type hints, Polars immutability, and refactor to avoid deep nesting. Master nonlocal, and you’ll write clean, stateful closures — but know when to replace them with better structure.
Next time you need state in a nested function — use nonlocal carefully. It’s Python’s cleanest way to say: “This inner function needs to remember and change its outer context.”