Python’s scope rules are surprisingly flexible, allowing you to modify variables in outer scopes with the nonlocal keyword, which most languages don’t offer.

Let’s see this in action. Imagine you have a function that needs to update a counter defined in an enclosing function, not the global scope.

def outer_function():
    count = 0
    print(f"Outer: initial count = {count}")

    def inner_function():
        # Without 'nonlocal', this would create a new local 'count'
        nonlocal count
        count += 1
        print(f"Inner: count after increment = {count}")

    inner_function()
    print(f"Outer: count after inner call = {count}")

outer_function()

Running this code produces:

Outer: initial count = 0
Inner: count after increment = 1
Outer: count after inner call = 1

This demonstrates that inner_function successfully modified the count variable belonging to outer_function.

The Problem: Variable Shadowing and Unintended Rebinding

When you define a variable inside a function, Python by default treats it as a local variable. If you try to assign a new value to a variable that exists in an outer scope (but not the global scope), Python will silently create a new local variable with the same name, effectively shadowing the outer one. This is a common source of bugs where you expect to modify an outer variable but end up creating a new, isolated one.

Consider this without nonlocal:

def outer_function_buggy():
    count = 0
    print(f"Outer: initial count = {count}")

    def inner_function_buggy():
        # This creates a NEW local 'count', it does NOT modify the outer one
        count = 10  # Assigning a value
        print(f"Inner: local count = {count}")

    inner_function_buggy()
    print(f"Outer: count after inner call = {count}") # The outer count remains unchanged

outer_function_buggy()

Output:

Outer: initial count = 0
Inner: local count = 10
Outer: count after inner call = 0

The count in outer_function_buggy remains 0 because inner_function_buggy created its own count.

The Solution: global vs. nonlocal

Python has two keywords to address this: global and nonlocal.

  • global: This keyword tells Python that a variable inside a function refers to a variable in the global scope. Any modifications will affect the single global variable.

    global_counter = 0
    
    def increment_global():
        global global_counter
        global_counter += 1
        print(f"Global counter: {global_counter}")
    
    increment_global()
    increment_global()
    

    Output:

    Global counter: 1
    Global counter: 2
    
  • nonlocal: This keyword tells Python that a variable inside a function refers to a variable in the nearest enclosing scope that is not global. This is crucial for nested functions where you want to modify variables in parent functions, not just the global namespace.

    The example at the beginning of this explanation uses nonlocal to modify count within outer_function from inner_function.

Scope Resolution Order (LEGB Rule)

Python resolves variable names using the LEGB rule:

  1. Local: The current function’s local scope.
  2. Enclosing Function Locals: Scopes of any enclosing functions, from nearest to farthest. This is where nonlocal operates.
  3. Global: The module’s global scope. This is where global operates.
  4. Built-in: Names in Python’s built-in scope (e.g., print, len).

When you read a variable, Python searches these scopes in order. When you assign a variable without global or nonlocal, Python assumes it’s a new local variable if it doesn’t already exist in the local scope. If it does exist locally, it modifies that local variable.

When to Use nonlocal

nonlocal is primarily used in scenarios involving nested functions, particularly when implementing:

  • Closures: Functions that "remember" the environment in which they were created.
  • Decorators: Functions that wrap other functions, often needing to maintain state or modify variables in the decorator’s scope.
  • Stateful functions: Functions that need to maintain state across calls by modifying variables in an outer, persistent scope.

Consider a factory function that creates counter functions:

def make_counter():
    count = 0
    def counter():
        nonlocal count
        count += 1
        return count
    return counter

counter1 = make_counter()
print(f"Counter 1: {counter1()}") # Output: Counter 1: 1
print(f"Counter 1: {counter1()}") # Output: Counter 1: 2

counter2 = make_counter() # Creates a *new* independent counter
print(f"Counter 2: {counter2()}") # Output: Counter 2: 1
print(f"Counter 1: {counter1()}") # Output: Counter 1: 3 (counter1 is unaffected by counter2)

The nonlocal count in counter ensures that each call to counter increments the count variable specific to the make_counter call that created it, not a new local one.

The most subtle aspect of nonlocal is that it only applies to variables in enclosing scopes that are not global. If you have a nonlocal variable and a global variable with the same name, nonlocal will only target the enclosing one, and global will only target the module-level one. You cannot use nonlocal to reach further out than the immediate enclosing function’s scope.

The next concept you’ll grapple with is how these scope rules interact with mutable versus immutable objects when modifying them within different scopes.

Want structured learning?

Take the full Python course →