Welcome back. In the previous lesson, you traced name lookup with the LEGB rule and saw the key assignment rule: an assignment inside a function normally makes that name local to that function. This explains why reading retries before writing retries += 1 can raise UnboundLocalError, even when a binding exists outside the function.
Now we will use that rule deliberately. By the end of this lesson, you will be able to update a variable held by an enclosing function using nonlocal, preserving state inside a closure rather than creating or modifying module-level global state.
The problem nonlocal solves
Consider a small stateful operation: a function that counts probe attempts for one particular device session.
def make_attempt_counter():
attempts = 0
def record_attempt():
attempts += 1
return attempts
return record_attempt
It is natural to expect record_attempt() to update the attempts defined by make_attempt_counter(). Instead, calling it fails:
counter = make_attempt_counter()
counter()
UnboundLocalError: cannot access local variable 'attempts' where it is not associated with a value
The inner function contains attempts += 1. As you learned previously, augmented assignment is still assignment. Python therefore classifies attempts as a local name inside record_attempt(). Before it can add one, it must read that local name, but no local value has yet been assigned.
The outer attempts is visible for reading, but assignment requires an explicit instruction:
def make_attempt_counter():
attempts = 0
def record_attempt():
nonlocal attempts
attempts += 1
return attempts
return record_attempt
nonlocal attempts tells Python:
Do not create a local binding named
attemptsin this inner function. Use the existing binding from an enclosing function scope.
Now the closure works as intended:
counter = make_attempt_counter()
print(counter()) # 1
print(counter()) # 2
print(counter()) # 3
The outer call to make_attempt_counter() has returned, but its attempts binding remains available because the returned record_attempt function closes over it. Each call to counter() modifies that same enclosing binding.
nonlocal Keyword | Python Tutorial
Watch “nonlocal Keyword | Python Tutorial” from Portfolio Courses for a compact visual walkthrough of the exact failure above: reading an enclosing value works, ordinary assignment creates an inner local, and nonlocal changes that behavior.
Watch the complete short video. Begin with reading outer state to establish the enclosing-scope case. Continue through the local rebinding problem, then watch the nonlocal fix. Focus on the difference between creating a new inner binding and updating the outer binding.
What nonlocal can—and cannot—target
A nonlocal declaration applies only inside a nested function. Its target must already be bound in an enclosing function scope—as a local variable or parameter.
def make_threshold_checker(limit):
checks = 0
def check(value):
nonlocal checks
checks += 1
return value <= limit, checks
return check
Here:
limitis captured from the enclosing scope and only read, so it needs no declaration.checksis rebound bychecks += 1, so it requiresnonlocal checks.- Both bindings belong to the particular call to
make_threshold_checker()that created the returned function.
The declaration can cover several names:
def make_status():
successes = 0
failures = 0
def record(ok):
nonlocal successes, failures
if ok:
successes += 1
else:
failures += 1
return successes, failures
return record
Place nonlocal near the start of the nested function, before any use or assignment of those names. Otherwise Python raises a SyntaxError, because it cannot reconcile an earlier local interpretation with a later nonlocal declaration.
Two invalid patterns matter most:
nonlocal count
At module level, this is invalid because there is no enclosing function scope.
def configure():
def update():
nonlocal count
This is also invalid if no surrounding function has already bound count. Unlike ordinary assignment, nonlocal cannot create an enclosing name lazily.
Python Scope and the LEGB Rule: Resolving Names in Your Code – Real Python
Read the “The nonlocal Statement” section in Real Python’s scope guide to consolidate the syntax rules and see the common SyntaxError cases.
In the section “The nonlocal Statement,” start at the core rule. Continue through the function() and nested() example, then read the two invalid uses that follow: module-level nonlocal and a nested function with no enclosing binding. Finish with the example beginning “Unlike global,” which shows why the target binding must already exist.
Which enclosing binding is modified?
When nested functions have several levels, nonlocal selects the nearest enclosing function scope that binds the name.
def outer():
mode = "outer"
def middle():
mode = "middle"
def inner():
nonlocal mode
mode = "changed by inner"
return mode
result = inner()
return result, mode
return middle(), mode
print(outer())
The result is:
(('changed by inner', 'changed by inner'), 'outer')
inner() modifies middle()’s mode, not outer()’s. The nearer binding wins, just as the nearest available binding wins during LEGB lookup.
This is a reason to choose distinct, meaningful names in deeply nested functions. Reusing broad names such as config, state, or result at several levels makes it needlessly hard to see what a nonlocal declaration will affect.
nonlocal versus global
Both nonlocal and global change assignment behavior, but their design implications are very different.
| Declaration | Assignment target | Must already exist? | Typical role |
|---|---|---|---|
| No declaration | Current function’s local scope | No | Temporary local work |
nonlocal name | Nearest enclosing function binding | Yes | Private state retained by a closure |
global name | Module-level namespace | No | Rare, explicitly shared module state |
Suppose a module has a global monitoring counter:
attempts = 1000
def make_attempt_counter():
attempts = 0
def record_attempt():
global attempts
attempts += 1
return attempts
return record_attempt
Despite appearing inside make_attempt_counter(), global attempts does not refer to its outer local variable. It specifically targets the module-level name. Calling the returned function increments the global counter from 1000 to 1001; the outer function’s local attempts = 0 is untouched.
That is exactly why global is the wrong tool for state that belongs to one returned function, one request, one connection, or one workflow instance. It introduces hidden sharing between otherwise unrelated calls.
By contrast, two closures made from the nonlocal version have independent state:
site_a_attempts = make_attempt_counter()
site_b_attempts = make_attempt_counter()
print(site_a_attempts()) # 1
print(site_a_attempts()) # 2
print(site_b_attempts()) # 1
Each call to make_attempt_counter() creates a distinct enclosing scope. This can be useful for lightweight per-target state in automation code, where a module-global counter would mix activity from unrelated targets.

Use the decision tree as a quick diagnostic, but begin with one precise question: which existing binding should this assignment change? If the honest answer is “a state value owned by the function that created this inner function,” nonlocal is appropriate.
Mutation is not rebinding
The previous lesson separated object mutation from name rebinding. That distinction remains essential inside closures.
A closure can mutate an enclosing mutable object without nonlocal:
def make_event_log():
events = []
def record(event):
events.append(event)
return list(events)
return record
record() does not assign to the name events. It resolves events in the enclosing scope, obtains the list object, and calls its .append() method. The object changes in place.
However, replacing the list means rebinding its name:
def make_event_log():
events = []
def record(event):
nonlocal events
events = events + [event]
return events
return record
Here, events + [event] creates a new list, and events = ... must update the enclosing binding so that later calls see the replacement list.
A subtle variation is worth recognizing:
events += [event]
Even though list.__iadd__ commonly mutates a list in place, += is still assignment syntax. Without nonlocal events, Python classifies events as local and the code fails before it can perform the operation. Prefer .append() or .extend() when in-place mutation is intended; use nonlocal when the design truly requires rebinding.
Python Closures: Common Use Cases and Examples – Real Python
Read the “Captured Variables” discussion in Real Python’s closures guide. It connects the scope rule to a practical distinction: immutable values such as integers are updated by rebinding, while mutable objects can often be changed in place.
In the “Captured Variables” section, begin at the update distinction. Then study the make_counter() example and the make_appender() example immediately after it. Compare why incrementing count requires nonlocal while calling items.append() does not.
A controlled closure-state pattern
A useful closure has a narrow purpose, a small amount of state, and a clear interface. For example, this factory produces independent retry gates:
def make_retry_gate(max_attempts):
used_attempts = 0
def allow_attempt():
nonlocal used_attempts
if used_attempts >= max_attempts:
return False
used_attempts += 1
return True
return allow_attempt
Usage:
primary_gate = make_retry_gate(max_attempts=2)
backup_gate = make_retry_gate(max_attempts=1)
print(primary_gate()) # True
print(primary_gate()) # True
print(primary_gate()) # False
print(backup_gate()) # True
print(backup_gate()) # False
The design is intentional:
max_attemptsis fixed configuration captured by the closure.used_attemptsis mutable state represented by an integer binding.- Only
used_attemptsneedsnonlocal. - Each gate owns its own state; there is no shared module-level counter.
- Callers cannot directly overwrite
used_attempts, which keeps the state transition inside one small operation.
This is appropriate for compact stateful behavior. If the state grows to require several operations, serialization, detailed inspection, or lifecycle management, a class or an explicit state object will generally be clearer. You will examine those design choices later in the course.
One boundary to keep in mind: nonlocal controls scope, not concurrency. If the same closure is called simultaneously from multiple threads, used_attempts += 1 is still a shared-state update and may require synchronization. For now, treat each closure as an isolated unit of state, not as automatically thread-safe code.
A practical debugging checklist
When an inner function appears not to update outer state:
- Identify whether the operation mutates an object or rebinds a name.
- If it rebinds a name, locate the intended owner of that binding.
- If the owner is an enclosing function, declare that name with
nonlocal. - Confirm that the enclosing binding exists before the inner function executes.
- Avoid replacing
nonlocalwithglobalmerely to suppress an error; that changes the ownership boundary of the state.
Key takeaways
nonlocal lets a nested function rebind a name owned by an enclosing function. It is the explicit counterpart to the assignment rule that otherwise creates a new local binding.
Remember:
- Reading an enclosing name needs no declaration.
- Rebinding an enclosing name, including through
+=, requiresnonlocal. - The target must already exist in an enclosing function scope.
nonlocalupdates the nearest enclosing binding with that name.- Mutating a captured list, dictionary, or set through methods such as
.append()does not itself requirenonlocal. - A closure with
nonlocalstate provides controlled, per-instance state without the hidden coupling of a mutable module global.
This completes the module’s exploration of Python’s object, call, and name-binding semantics. Next, you will build function factories deliberately, using closures to capture configuration and produce specialized callable behavior.
Can't find a good explanation? Sign up and we'll make it for you
Sign up