The nonlocal keyword is a crucial tool for managing state within nested functions, enabling inner functions to modify variables from their enclosing (non-global) scope. It was introduced in Python 3 to resolve the ambiguity and limitations of using mutable objects for state changes and to provide a cleaner alternative to the global keyword for nested scopes.

The Problem nonlocal Solves

Without nonlocal, an inner function can read variables from an enclosing scope, but any assignment to a name will create a new local variable within that inner function’s scope, shadowing the name from the outer scope. This often leads to unexpected UnboundLocalError exceptions.

def counter_initial():
    count = 0

    def increment():
        # This assignment makes 'count' a new local variable.
        # The outer 'count' is now inaccessible and uninitialized here.
        count = count + 1  # 🚫 UnboundLocalError: local variable 'count' referenced before assignment
        return count

    return increment

my_counter = counter_initial()
print(my_counter())  # Raises UnboundLocalError

The error occurs because the assignment count = ... tells Python that count is a local variable for the entire increment() function. When it tries to evaluate the right-hand side (count + 1), it finds the local count hasn’t been assigned a value yet.

How nonlocal Works

The nonlocal declaration explicitly tells the Python interpreter that a given variable name refers to a variable in the nearest enclosing scope that is not global. It allows you to rebind that name to a new value, effectively modifying the variable from the outer function.

def counter_nonlocal():
    count = 0  # This variable is in the enclosing scope.

    def increment():
        nonlocal count  # Declare 'count' as nonlocal. It refers to the variable above.
        count += 1      # Now we can modify it.
        return count

    return increment

my_counter = counter_nonlocal()
print(my_counter())  # Output: 1
print(my_counter())  # Output: 2
print(my_counter())  # Output: 3

Each call to my_counter() invokes the increment() function, which modifies the count variable that persists within the closure of the counter_nonlocal() function call. This is the canonical example of using nonlocal to create stateful function objects.

nonlocal vs. Global

It is vital to distinguish nonlocal from global. nonlocal looks for the variable in the nearest enclosing scope (skipping the local scope), while global looks for it in the global module scope. Using global inside a nested function to modify a variable from an enclosing function would not work as intended and is a common pitfall.

def counter_bad_global():
    count = 0  # This is *not* a global variable.

    def increment():
        global count  # ❌ Wrong! This looks for 'count' in the global module scope.
        count += 1
        return count

    return increment

my_counter = counter_bad_global()
print(my_counter())  # Output: 1
print(my_counter())  # Output: 2
# But the state is stored in the global scope, causing unintended side effects.
print(count)  # Output: 2 - This is now a global variable, polluting the module namespace.

The Pre-nonlocal Workaround: Using Mutable Objects

Before Python 3, the common workaround was to use a mutable object, like a list or a dictionary. Since the inner function isn’t reassigning the name itself but is instead modifying the contents of the object the name refers to, no nonlocal declaration is needed.

def counter_mutable():
    container = [0]  # A mutable list containing the count.

    def increment():
        container[0] += 1  # Modifying the element inside the mutable object.
        return container[0]

    return increment

my_counter = counter_mutable()
print(my_counter())  # Output: 1
print(my_counter())  # Output: 2

While this works, it is less readable and more error-prone than using nonlocal. The nonlocal keyword provides a clear and explicit intent to modify the enclosing variable.

Scope Resolution and the LEGB Rule

The nonlocal keyword interacts directly with Python’s LEGB (Local, Enclosing, Global, Built-in) name lookup rule. When you declare a variable nonlocal, you are instructing the interpreter to skip the ‘L’ (Local) level of the LEGB rule during assignment and instead find the name in the nearest ‘E’ (Enclosing) scope. If no such variable is found in any enclosing scope, a SyntaxError is raised at compile time.

def outer():
    def inner():
        nonlocal x  # 🚫 SyntaxError: no binding for nonlocal 'x' found
        x = 10

    inner()

outer()

Best Practices and Common Pitfalls

  1. Clarity over Cleverness: Use nonlocal for its intended purpose—creating closures that manage simple, contained state. Avoid using it to create complex, hard-to-follow data flows between deeply nested functions.
  2. One-Way Street: nonlocal can only reference variables from enclosing scopes; it cannot create them. The variable must already exist in an enclosing scope. The nonlocal declaration itself does not initialize the variable.
  3. Not for Globals: Remember that nonlocal will never refer to a global variable. If you need to modify a global variable from within a nested function, you must use the global keyword (though this is often a design smell).
  4. Readability: For more complex state, consider using a class. A class with instance variables and methods often provides a more readable and maintainable structure than a closure with multiple nonlocal variables.
# Often clearer than a complex closure
class Counter:
    def __init__(self):
        self.count = 0

    def increment(self):
        self.count += 1
        return self.count

my_counter = Counter()
print(my_counter.increment())  # Output: 1