Definitions - nonlocal variables refer to variables in Python that are defined in an enclosing (outer) function scope and accessed or modified from a nested (inner) function. By default, variables in an inner function are local to that function — assignment creates a new local variable, and reading looks up the nearest enclosing scope. The nonlocal keyword (introduced in Python 3) explicitly declares that a name refers to a variable in the nearest enclosing scope (excluding global), allowing both reading and modification of that outer variable. In 2026, nonlocal is a key tool for writing stateful closures, accumulators, factory functions, and clean nested logic in data pipelines, ML workflows, GUI/async code, and production systems — but overuse signals poor design; prefer parameters, return values, classes, or immutability for shared state.
Here’s a complete, practical guide to nonlocal variables in Python: how they work, 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 nonlocal usage — inner function modifies outer function’s variable with nonlocal.
def outer_function():
x = 10
def inner_function():
nonlocal x # declare x refers to outer scope
x += 5
print("Inner:", x)
inner_function()
print("Outer:", x)
outer_function()
# Inner: 15
# Outer: 15
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.
Nonlocal variables allow nested functions to modify enclosing scope variables — essential for stateful closures. In 2026, use nonlocal 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.”