Create your own
Lesson illustration

Modifying Enclosing Scope State with `nonlocal`

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 attempts in 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:

  • limit is captured from the enclosing scope and only read, so it needs no declaration.
  • checks is rebound by checks += 1, so it requires nonlocal 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.

DeclarationAssignment targetMust already exist?Typical role
No declarationCurrent function’s local scopeNoTemporary local work
nonlocal nameNearest enclosing function bindingYesPrivate state retained by a closure
global nameModule-level namespaceNoRare, 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.

A decision tree for assignments inside a Python function: use `nonlocal` when updating a name bound in an enclosing function, use `global` only when deliberately updating a module-level name, and use no declaration for a new local binding.

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_attempts is fixed configuration captured by the closure.
  • used_attempts is mutable state represented by an integer binding.
  • Only used_attempts needs nonlocal.
  • 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:

  1. Identify whether the operation mutates an object or rebinds a name.
  2. If it rebinds a name, locate the intended owner of that binding.
  3. If the owner is an enclosing function, declare that name with nonlocal.
  4. Confirm that the enclosing binding exists before the inner function executes.
  5. Avoid replacing nonlocal with global merely 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 +=, requires nonlocal.
  • The target must already exist in an enclosing function scope.
  • nonlocal updates the nearest enclosing binding with that name.
  • Mutating a captured list, dictionary, or set through methods such as .append() does not itself require nonlocal.
  • A closure with nonlocal state 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