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 inf_localsplus. $O(1)$ constant time. - Enclosing Scope (
LOAD_DEREF/STORE_DEREF): AccessesPyCellObjectpointers shared between enclosing and inner functions. - Global Scope (
LOAD_GLOBAL): Dictionary hash lookup infunc_globals(globals()). - Built-in Scope (
LOAD_GLOBALfallback): Fallback dictionary hash lookup inbuiltins.__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 ofxto target the module-level Global scope.nonlocal x: Instructs the compiler to bypass Local scope and target the nearest variablexin 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