Scope, LEGB Resolution, global & nonlocal

Variable name resolution in Python follows the strict LEGB Rule (Local $\rightarrow$ Enclosing $\rightarrow$ Global $\rightarrow$ Built-in). At compile time, CPython analyzes symbol scopes to emit optimized variable lookup opcodes.

This chapter details the LEGB resolution sequence, LOAD_FAST vs LOAD_GLOBAL vs LOAD_DEREF opcodes, UnboundLocalError mechanics, and the explicit scoping controls global and nonlocal.


1. The LEGB Resolution Rule

When a variable name is evaluated, CPython searches four nested scopes in strict order:

The LEGB Scope Resolution Sequence:

[ L ] Local Scope       --> Names assigned inside current function / comprehension
          |
          v (If not found locally)
[ E ] Enclosing Scope   --> Names in outer enclosing functions (Closures)
          |
          v (If not found in enclosing scope)
[ G ] Global Scope      --> Module-level names in current file (__main__ or module dict)
          |
          v (If not found in module globals)
[ B ] Built-in Scope   --> Pre-loaded built-ins (builtins module: len, range, ValueError)

If the name is not found in any of the four LEGB layers, CPython raises a NameError.


2. Bytecode Opcode Performance: LOAD_FAST vs. LOAD_DEREF

Scope resolution isn’t determined at runtime via string hashing for every scope layer; CPython’s compiler analyzes scope at compile time:

  • Local Scope (LOAD_FAST / STORE_FAST): Fast C-array index lookup in f_localsplus. $O(1)$ constant time.
  • Enclosing Scope (LOAD_DEREF / STORE_DEREF): Accesses PyCellObject pointers shared between enclosing and inner functions.
  • Global Scope (LOAD_GLOBAL): Dictionary hash lookup in func_globals (globals()).
  • Built-in Scope (LOAD_GLOBAL fallback): Fallback dictionary hash lookup in builtins.__dict__.

3. The UnboundLocalError Trap

A common pitfall occurs when a variable is referenced before assignment inside a function where that same variable name is assigned later in the function body:

x = 10  # Global variable

def process():
    print(x)  # TRAP: UnboundLocalError: local variable 'x' referenced before assignment!
    x = 20    # Compiler sees assignment and marks 'x' as LOCAL for the entire function!

process()

Why it Happens:

CPython’s compiler analyzes the entire function body at compile time. Because x = 20 exists anywhere inside process(), CPython marks x as a Local variable for the entire function and emits LOAD_FAST. At execution time, print(x) attempts to load x from the local array before it has been bound, triggering UnboundLocalError.


4. Scoping Modifiers: global vs. nonlocal

  • global x: Instructs the compiler to bypass Local and Enclosing scope bounds, forcing reads and assignments of x to target the module-level Global scope.
  • nonlocal x: Instructs the compiler to bypass Local scope and target the nearest variable x in an Enclosing function scope (required when mutating closure state).
def outer():
    count = 0  # Enclosing variable

    def inner():
        nonlocal count  # Bypasses local scope, binds to outer's count
        count += 1
        return count

    return inner
Display Options
Appearance
Text Size
100%