I still remember the first time a closure I wrote “forgot” its state. I expected a nested function to update a counter in its outer function, but instead I got a new local variable and a runtime error. That moment taught me a hard lesson: understanding scope is not optional in Python. If you ship production code with nested functions, decorators, callbacks, or stateful factories, you will run into this. The nonlocal keyword is the tool that makes those patterns predictable. You can think of it as a pointer to the nearest enclosing function’s variable—neither local to the current function, nor global to the module. That middle ground is where a lot of modern Python lives: tiny composable functions that carry state without a class.
In this post I’ll show you how nonlocal actually works, where it’s the best fit, where it’s a trap, and how I use it to build reliable stateful behavior in real systems. You’ll get runnable examples, edge cases, modern patterns I use in 2026, and a clear mental model you can apply the next time a nested function behaves “wrong.”
The Scope Ladder I Keep in My Head
When I reason about scope in Python, I use a four-step ladder: local, enclosing, global, built-in. If a name is referenced, Python searches in that order. nonlocal only touches the enclosing step. That’s it.
Here’s the ladder I keep in my head:
- Local scope: variables defined inside the current function
- Enclosing scope: variables in the nearest outer function (if nested)
- Global scope: module-level names
- Built-in scope:
len,range,print, etc.
nonlocal says: “When I assign to this name, bind it to the nearest enclosing function’s variable.” It does not reach the module, and it does not create the name if it doesn’t already exist in an enclosing function.
A small mental model helps: nonlocal is like a scoped reference to a parent variable, but only for function nesting. It is not a general scope escape hatch.
The First Real Example: Fixing a Broken Closure
If you’ve ever built a simple counter with a nested function, you might have hit this error:
def make_counter():
count = 0
def increment():
count += 1
return count
return increment
Calling increment() raises UnboundLocalError because Python sees an assignment to count inside increment, assumes it is local, and then reads it before it is assigned. The fix is exactly what nonlocal is for:
def make_counter():
count = 0
def increment():
nonlocal count # bind to enclosing variable
count += 1
return count
return increment
counter = make_counter()
print(counter()) # 1
print(counter()) # 2
print(counter()) # 3
This is the canonical use case: keep state in a closure without a class. It’s clean, fast, and easy to test.
How nonlocal Behaves with Multiple Nested Levels
When there are multiple nested functions, nonlocal binds to the nearest enclosing scope that has the name. That’s not always the outermost scope. The nearest one wins.
def build_labeler():
label = "root"
def make_child():
label = "child"
def setlabel(newlabel):
nonlocal label
label = new_label
return label
return set_label
setter = make_child()
print(setter("leaf")) # leaf
print(setter("twig")) # twig
print(label) # root
build_labeler()
I find it helpful to visualize this as a chain: setlabel sees label in makechild, not in build_labeler. nonlocal attaches to the nearest enclosing binding, which can be surprising if you assume it always reaches the outermost function.
nonlocal vs global in Practice
Both keywords change the binding behavior for assignments, but they operate on different levels of the scope ladder. I keep this comparison table handy when teaching teammates:
Affects
Can modify module variable?
—
—
nonlocal Enclosing function scope
No
global Module (global) scope
YesHere’s a concrete example that shows both at work:
status = "idle"
def create_job():
state = "queued"
def start():
nonlocal state
global status
state = "running"
status = "busy"
return state
start()
return state
print(create_job()) # running
print(status) # busy
My rule of thumb: use nonlocal for self-contained, testable state in closures. Use global only when you deliberately want module-level state (config flags, singletons, or legacy interop). In modern codebases, global is the exception.
The Hard Rules: Where nonlocal Fails
I’ve seen engineers lose time on these rules, so I’ll spell them out:
- The name must already exist in an enclosing function.
- You cannot use
nonlocalat module level. - You cannot use
nonlocalto target a global variable. - You cannot use it in a function with no enclosing function.
This code fails because total isn’t defined in any enclosing function:
def increment():
nonlocal total
total += 1
And this fails because the only binding is global:
count = 0
def update():
nonlocal count # SyntaxError: no binding for nonlocal ‘count‘ found
count += 1
If you see “no binding for nonlocal found,” the fix is not to force it—it’s to move that variable into an enclosing function or refactor into a class.
A Practical Pattern: Stateful Functions Without Classes
I love classes, but sometimes I just want a tiny state machine without the ceremony. nonlocal gives me that.
Here’s a rolling window average without a class:
def rollingaverage(windowsize):
values = []
def add(value):
nonlocal values
values.append(value)
if len(values) > window_size:
values.pop(0)
return sum(values) / len(values)
return add
avg5 = rolling_average(5)
print(avg5(10)) # 10.0
print(avg5(20)) # 15.0
print(avg5(30)) # 20.0
This pattern is perfect for lightweight data processing, event throttling, or in-memory metrics. If the state grows large or you need serialization, a class might be better, but I reach for nonlocal first when I want minimal overhead.
When I Don’t Use nonlocal
I do not use nonlocal when:
- The state needs to be shared across multiple functions or modules
- The state needs to be inspected or modified externally
- The logic grows beyond a few lines
- The variable must outlive the function that created it
In those cases I usually use a class, a dataclass, or a small state object. It’s easier to test, easier to document, and clearer for new contributors.
Traditional vs Modern Approach
Here’s how I decide between a closure and a class in 2026:
Traditional Approach
My Recommendation
—
—
Class with call
nonlocal Use nonlocal for brevity
Class attributes
Use a class or dataclass
Manual dict export
asdict Use dataclass
Callback + globals
Use closure + nonlocalThe key is readability. If the closure’s logic stays short, nonlocal wins. If it grows, the class is clearer.
Common Mistakes I See (and How I Fix Them)
Here are the mistakes I see in code reviews and how I correct them:
- Accidental local rebind
def outer():
name = "Ada"
def inner():
name = "Grace" # shadows outer
return name
Fix: add nonlocal name if you meant to mutate the outer variable.
- Using
nonlocalfor global state
value = 5
def outer():
def inner():
nonlocal value
Fix: use global value or move value into outer.
- Trying to mutate immutable values without binding
def outer():
total = 0
def add(x):
total + x
return total
Fix: nonlocal total and then rebind with total += x.
- Using
nonlocalinside loops with nested functions
The trap: each nested function shares the same outer variable. If you want a distinct value, capture it explicitly.
def make_actions():
actions = []
for i in range(3):
def action():
return i
actions.append(action)
return actions
# All return 2
Fix: bind i as a default parameter in the nested function if you need per-iteration values.
The LEGB Rule with nonlocal Under the Hood
When Python compiles a function, it determines which names are local based on assignment. nonlocal changes that compilation decision. In bytecode terms, the variable becomes a “cell variable,” and the inner function holds a reference to it.
You don’t need to read bytecode to use this, but it explains why errors happen at compile time rather than runtime. That’s also why nonlocal must appear before the name is used—it changes the compiler’s analysis.
Here is a quick way to visualize it:
- Without
nonlocal, assignments make a name local to the current function. - With
nonlocal, assignments target the nearest enclosing binding. - If there’s no enclosing binding, Python throws a syntax error before you run the code.
If you ever wonder why nonlocal errors are syntax errors rather than runtime errors, this is the reason.
Edge Cases: Mutable vs Immutable Outer Variables
One subtle point: you only need nonlocal when you rebind the name. If the name points to a mutable object, you can mutate it without nonlocal.
def outer():
items = []
def add(item):
items.append(item) # no nonlocal needed
return items
return add
But if you reassign the variable itself, you do need nonlocal:
def outer():
items = []
def reset():
nonlocal items
items = [] # rebinds name
return items
return reset
This distinction is a frequent source of confusion. My rule: if you use = on the name, you need nonlocal (unless the name is local anyway). If you mutate the object, you don’t.
nonlocal in Real-World Patterns
Here are a few patterns I use in modern codebases where nonlocal is a clean fit.
1) Retry with Backoff Counter
def retrypolicy(maxattempts):
attempts = 0
def should_retry():
nonlocal attempts
attempts += 1
return attempts <= max_attempts
return should_retry
shouldretry = retrypolicy(3)
print([shouldretry() for in range(5)]) # [True, True, True, False, False]
This lets you inject a policy function into retry logic without creating a class or global variable.
2) A/B Feature Toggle Closure
def featuretoggle(featurename):
enabled = False
def set_enabled(value):
nonlocal enabled
enabled = bool(value)
def is_enabled():
return enabled
return setenabled, isenabled
setcheckout, ischeckoutenabled = featuretoggle("checkout")
set_checkout(True)
print(ischeckoutenabled()) # True
A small closure can be perfect for unit tests and configuration injection.
3) Metrics Buffer Without External State
def buffered_metrics(limit):
buffer = []
def record(value):
nonlocal buffer
buffer.append(value)
if len(buffer) >= limit:
snapshot = buffer
buffer = []
return snapshot
return None
return record
record = buffered_metrics(3)
print(record(10)) # None
print(record(20)) # None
print(record(30)) # [10, 20, 30]
This is a neat way to batch events without any global variables or classes.
Performance Notes (What Actually Matters)
In practice, nonlocal has negligible overhead compared to normal variable access. You might see a tiny difference in microbenchmarks because cell variables are accessed via a different opcode, but it’s not a bottleneck in real systems. In typical application code, the difference is well below a millisecond per thousand operations.
The real performance concern is not nonlocal—it’s your algorithm and your I/O. If nonlocal makes your logic simpler and your code clearer, that’s a net win.
Testing and Debugging Tips
When I test closures with nonlocal, I prefer explicit behavior tests:
- Verify initial state
- Call multiple times to verify mutation
- Ensure state isolation between separate closure instances
Example:
def makethresholdchecker(threshold):
count = 0
def check(value):
nonlocal count
if value > threshold:
count += 1
return count
return check
checkera = makethreshold_checker(10)
checkerb = makethreshold_checker(10)
print(checker_a(12)) # 1
print(checker_a(9)) # 1
print(checker_b(12)) # 1 (separate state)
If you see state leaking across instances, you probably accidentally used a global or a shared mutable object.
Common “Should I Use nonlocal?” Checklist
When I’m deciding, I run through this quick checklist:
- Do I need state that lives across calls? If yes,
nonlocalis a candidate. - Is the state only relevant inside this small group of nested functions? If yes,
nonlocalfits. - Will this code be easier to read than a class? If yes, use
nonlocal. - Does the state need to be accessed from outside? If yes, use a class or object.
That’s all I need for 90% of decisions.
A Simple Analogy That Actually Helps
I explain nonlocal like this: imagine a shared whiteboard in the next room. Your inner function writes on that whiteboard. It’s not your current desk (local scope), and it’s not the building lobby board (global scope). It’s the nearest shared board that belongs to the enclosing function. nonlocal is the key that lets you write there instead of creating a new sticky note on your desk.
That analogy makes the nearest-enclosing rule easy to remember.
Edge Case: nonlocal with async and await
nonlocal works the same in async functions. The state still lives in the closure, but you need to be careful with concurrency. If multiple tasks share the same closure instance, you can get race conditions.
import asyncio
def makeasynccounter():
count = 0
async def increment():
nonlocal count
await asyncio.sleep(0.01)
count += 1
return count
return increment
If you call increment() concurrently, the updates can interleave. In those cases, I wrap the state mutation with an asyncio.Lock or use a class with explicit locking. nonlocal is not the issue—the shared state is.
nonlocal in Decorators
Decorators are another place where nonlocal shines, especially when I need to keep per-function state without building a class. Here’s a simple rate-limited decorator that uses nonlocal to track recent calls.
import time
def ratelimit(callsper_second):
mininterval = 1.0 / callsper_second
last_call = 0.0
def decorator(fn):
def wrapper(args, *kwargs):
nonlocal last_call
now = time.time()
elapsed = now - last_call
if elapsed < min_interval:
time.sleep(min_interval - elapsed)
last_call = time.time()
return fn(args, *kwargs)
return wrapper
return decorator
@rate_limit(5)
def ping():
return "pong"
This keeps the lastcall state inside the decorator factory. Every function you decorate gets its own independent lastcall variable, which is exactly what you want for throttling. When I see bugs in rate-limiting code, they almost always come from accidental global variables, not nonlocal closures.
nonlocal in Callbacks and Event Handlers
Callbacks are another natural home for nonlocal. I use them when I need a light wrapper around event state.
def onchangetracker():
last_value = None
def onchange(newvalue):
nonlocal last_value
if newvalue != lastvalue:
lastvalue = newvalue
return True
return False
return on_change
tracker = onchangetracker()
print(tracker("A")) # True
print(tracker("A")) # False
print(tracker("B")) # True
This pattern keeps state private and makes it impossible for a caller to mutate last_value directly. That’s a small but meaningful safety win.
A Deeper Example: A Configurable Retry Wrapper
I’ll build this one in full because it reflects a real pattern from production code. I want a retry wrapper that carries its own counter, delay strategy, and last error. I don’t want a class. I do want testable behavior.
import time
def makeretry(maxattempts, base_delay):
attempts = 0
last_error = None
def reset():
nonlocal attempts, last_error
attempts = 0
last_error = None
def run(fn, args, *kwargs):
nonlocal attempts, last_error
while attempts < max_attempts:
try:
attempts += 1
return fn(args, *kwargs)
except Exception as exc:
last_error = exc
time.sleep(base_delay * attempts)
raise last_error
return run, reset
run, reset = make_retry(3, 0.1)
Here I use nonlocal for two variables. This is where it really helps: I can expose a small API (run, reset) and still keep state private. It’s a clean compromise between “globals everywhere” and “everything is a class.”
nonlocal and Type Hints
Type hints don’t change runtime behavior, but they can make nonlocal closures more readable. A pattern I use is to annotate the outer variable and keep inner functions clean.
from typing import Callable
def make_multiplier(factor: int) -> Callable[[int], int]:
total: int = 0
def multiply(x: int) -> int:
nonlocal total
total += x
return total * factor
return multiply
In this example, total is clearly typed and the inner function stays readable. I also like this because the type hint communicates intent: total is part of the closure’s state, not a loop-local scratch variable.
nonlocal with Dataclasses (Hybrid Pattern)
Sometimes I combine a small dataclass with a nonlocal closure to get the best of both worlds: lightweight state and a clean function interface.
from dataclasses import dataclass
@dataclass
class Stats:
count: int = 0
total: float = 0.0
def makestatscollector():
stats = Stats()
def add(value: float) -> float:
stats.count += 1
stats.total += value
return stats.total / stats.count
def snapshot() -> Stats:
return stats
return add, snapshot
Here, I don’t need nonlocal because I only mutate the dataclass fields, not rebind stats. But if I ever want to reset the object, I can use nonlocal stats and reassign it. This hybrid approach is a nice middle ground when I want structured state but still prefer a closure interface.
The “Late Binding” Trap and nonlocal
nonlocal doesn’t solve late binding in loops, and I still see it bite teams regularly. The issue is that inner functions reference the same variable, not its value at definition time.
def make_printers():
printers = []
for i in range(3):
def printer():
return i
printers.append(printer)
return printers
All three printers return 2. The fix is to capture the value at definition time:
def make_printers():
printers = []
for i in range(3):
def printer(i=i):
return i
printers.append(printer)
return printers
nonlocal is not relevant here. The issue is late binding of i. I call it out because people reach for nonlocal as a fix and then get confused when it doesn’t help.
nonlocal and Exceptions: Capturing Error State
Sometimes I want to keep the last exception in a closure for introspection or retry logic. nonlocal is a natural fit.
def guarded(fn):
last_error = None
def run(args, *kwargs):
nonlocal last_error
try:
return fn(args, *kwargs)
except Exception as exc:
last_error = exc
return None
def error():
return last_error
return run, error
This is a small but powerful pattern for ETL pipelines or background jobs where you need to keep error state near the function that owns it.
Common Pitfalls in Production Code
Here are a few more pitfalls I see in real systems, with fixes that keep the code stable:
1) Multiple nonlocal Variables Without Clear Grouping
If you have many nonlocal variables in a function, the closure might be doing too much. Consider grouping state in a dict or dataclass.
def make_accumulator():
state = {"sum": 0, "count": 0}
def add(x):
state["sum"] += x
state["count"] += 1
return state["sum"] / state["count"]
return add
Now you don’t need nonlocal and the state is explicit.
2) Naming Collisions Between Outer and Inner Functions
I’ve seen closures where nonlocal accidentally binds the wrong variable because of shadowing. If you reuse names, be very intentional.
def outer():
status = "outer"
def inner():
status = "inner" # shadows outer
def mutate():
nonlocal status
status = "changed"
return status
return mutate
This mutates the inner status, not the outer. Rename or restructure to avoid confusion.
3) Hidden State Changes in Lambdas
Lambdas can’t contain statements, so you can’t use nonlocal inside them directly. If you need mutation, use a named nested function. This is another reason I keep lambdas short and stateless.
Alternative Approaches (and When I Prefer Them)
nonlocal is one tool. It’s not always the best one. Here are the main alternatives and when I use them.
1) Classes with call
If the logic is more than a screenful, I often use a tiny class. It communicates intent and scales better.
class Counter:
def init(self):
self.count = 0
def call(self):
self.count += 1
return self.count
This is clear and debuggable. It also makes it easier to add methods later.
2) Mutable Containers
Sometimes a dict or list is the simplest way to share state without nonlocal:
def make_counter():
state = {"count": 0}
def increment():
state["count"] += 1
return state["count"]
return increment
This avoids nonlocal, but it can be less explicit about what’s happening if you overuse it. I reach for this when I want to store several related values and I don’t want a class.
3) functools.partial and Pure Functions
If I can make a function pure and parameterize it, I often do. This removes state entirely, which is always a win.
from functools import partial
def add(x, y):
return x + y
add_five = partial(add, 5)
No nonlocal needed, no state to mutate, and very testable.
4) itertools and Generators
If you want sequence-like state, generators can be cleaner than closures.
def counter():
n = 0
while True:
n += 1
yield n
c = counter()
print(next(c))
print(next(c))
Generators keep state internally without any explicit nonlocal or class. I prefer them when the state is naturally a sequence.
Debugging Tips I Actually Use
When a nonlocal closure behaves strangely, I do these things in order:
- Print
closureand cell values. It’s a fast sanity check. - Search for accidental shadowing. Most bugs come from redeclaring the same name.
- Add explicit tests for state isolation. If two closures share state accidentally, I want to see it in tests.
Here’s a quick snippet that reveals closure contents:
def make_counter():
count = 0
def inc():
nonlocal count
count += 1
return count
return inc
c = make_counter()
print(c.closure[0].cell_contents) # 0
print(c())
print(c.closure[0].cell_contents) # 1
I don’t ship code with this, but I absolutely use it while debugging.
Performance Considerations: Ranges, Not Microseconds
I mentioned earlier that nonlocal overhead is negligible. Here’s a practical framing I use when teams worry about it:
- Accessing a local variable is the fastest.
- Accessing a
nonlocalcell variable is slightly slower, but still tiny in absolute terms. - Accessing a global variable is often slower than either, especially if it misses locals first.
In real systems, the difference is drowned out by I/O, logging, and data processing. If you’re in the 1-10 micro-optimizations per million operations range, you’re already beyond the point where nonlocal matters. In that world, you should focus on algorithmic changes, caching, batching, or eliminating unnecessary work.
nonlocal with Recursive Functions
This is a niche case, but I’ve used it in recursive algorithms where I need to accumulate state without passing it through every call. For example, a tree traversal that counts nodes:
class Node:
def init(self, value, left=None, right=None):
self.value = value
self.left = left
self.right = right
def count_nodes(root):
count = 0
def walk(node):
nonlocal count
if not node:
return
count += 1
walk(node.left)
walk(node.right)
walk(root)
return count
This is clean and avoids threading count through every call. I still use it sparingly; sometimes passing a value explicitly is clearer, especially for less experienced readers.
nonlocal in a Realistic Pipeline Step
Here’s a more production-like example: a parser that needs to track warnings and errors across multiple nested steps.
def make_parser():
warnings = 0
errors = 0
def parse_line(line):
nonlocal warnings, errors
if not line.strip():
warnings += 1
return None
if "ERROR" in line:
errors += 1
return None
return line.strip()
def stats():
return {"warnings": warnings, "errors": errors}
return parse_line, stats
This gives me a lightweight parser with internal counters and a clean stats API. It scales well for small, testable utilities.
The Mental Model That Keeps Me Sane
When I’m using nonlocal, I keep this short model in my head:
- The outer function creates a frame with variables.
- The inner function captures a reference to those variables, not a copy.
nonlocaltells Python that assignment should update that reference.
This is why nonlocal works for state: both functions are talking about the same variable cell, not separate copies. If you hold onto that model, most confusion disappears.
nonlocal and Readability: My Practical Limits
This is subjective, but I have a limit: if I need more than 2-3 nonlocal declarations in a single inner function, I probably want a class or a state object. The point of nonlocal is clarity. If it becomes a tangle, it failed the readability test.
A rule I use in reviews: if a closure is responsible for more than one “kind” of behavior (e.g., parsing, caching, logging, retrying), I break it apart or move it into a class. It’s not that nonlocal can’t handle it; it’s that people can’t.
A Section I Wish I Had Earlier: Why nonlocal Exists
It’s easy to think of nonlocal as a niche feature, but it exists because closures are a core part of Python’s functional side. When you use decorators, higher-order functions, partial application, or callback registration, closures are unavoidable. nonlocal makes closures practical.
Without it, you’d be forced into classes or mutable containers for every tiny stateful function. That’s a lot of ceremony. nonlocal is Python’s compromise: it keeps the function interface simple while letting you mutate state safely inside a well-defined scope.
nonlocal vs Global State in Tests
If you test stateful code, you probably already know this: global state makes tests flaky. nonlocal is a nice alternative because it scopes state to the closure instance.
Here’s the testing difference:
- With globals, you must reset state between tests.
- With
nonlocal, each test can create a new closure instance and start clean.
That’s one reason I prefer nonlocal over module-level globals for counters, flags, and caches in small utilities.
A Practical “Do / Don’t” Table
This is the distilled version I keep in my own notes.
nonlocal when… Avoid nonlocal when…
—
You need shared state across modules
The logic is long or multi-purpose
You need external inspection or serialization
A class would be clearer for readersI don’t treat this as a rulebook, but it keeps me honest.
Wrapping Up: What I Want You to Remember
If you remember nothing else, remember this: nonlocal changes where assignments bind. It doesn’t make names magically available, and it doesn’t turn local variables into globals. It is a precise, targeted tool for one problem: mutating a variable from an enclosing function.
Used well, it makes closures clean, testable, and expressive. Used poorly, it creates confusing shadowing and brittle behavior. The mental model is simple: local, enclosing, global, built-in. nonlocal targets the enclosing step and nothing else.
The next time a nested function “forgets” its state, don’t panic. Check where the name is bound, decide whether a closure is the right tool, and apply nonlocal with intention. It’s not a trick. It’s a straightforward, powerful feature once you internalize the rules.


