How does Python's LEGB scoping rule work?
Quick Answer
Python resolves a name by searching, in order: **L**ocal (current function) → **E**nclosing (any enclosing function's scope, for closures) → **G**lobal (module level) → **B**uilt-in (`builtins` module). Assignment inside a function makes a name local by default *for that entire function body*, unless declared `nonlocal` or `global` — which is why assigning to a name before using it can raise `UnboundLocalError`.
Detailed Answer
The four scopes, in lookup order
x = "global"
def outer():
x = "enclosing"
def inner():
x = "local"
print(x) # 'local' -- found in Local scope
inner()
print(x) # 'enclosing'
outer()
print(x) # 'global'
print(len) # built-in, found in Built-in scope
When Python looks up a bare name, it checks Local, then Enclosing function scopes (innermost to outermost), then Global (module), then Built-in — the first scope where the name is bound wins.
The gotcha: assignment makes a name local for the whole function
Python decides whether a name is local to a function at compile time, by scanning the function body for assignments — not by checking whether the assignment has "already happened" at runtime.
count = 0
def increment():
print(count) # UnboundLocalError!
count = count + 1
Because count = ... appears anywhere in increment, Python treats count
as local for the entire function body — including the print(count) line
before the assignment. It never falls back to the global count.
Fixing it: global and nonlocal
count = 0
def increment():
global count
count += 1 # now refers to the module-level count
def make_counter():
total = 0
def add(n):
nonlocal total # refers to make_counter's `total`, not a new local
total += n
return total
return add
globalbinds a name to the module-level scope.nonlocalbinds a name to the nearest enclosing function scope (not global) — this is what makes stateful closures possible.
Why this matters for closures
The "E" in LEGB is exactly what lets a nested function remember variables
from its enclosing function after that function has returned — the classic
closure pattern (make_counter above). Without nonlocal, a nested
function can read an enclosing variable freely, but assigning to it
creates a new local instead of updating the enclosing one.
Interview-ready summary: Name resolution follows Local → Enclosing →
Global → Built-in, and whether a name is "local" is decided statically by
scanning for assignments in the function body — which is why referencing a
name before assigning it in the same function raises UnboundLocalError
instead of falling back to an outer scope. global and nonlocal are the
explicit escape hatches for writing to an outer scope.