How Python finds your variables
When you write a name like x, Python has to decide which x you mean. The rule it uses
is called LEGB, and almost every confusing scope bug — UnboundLocalError, closures
that "capture the wrong value", global/nonlocal mix-ups — comes from misunderstanding
it. This guide walks through name resolution from the ground up.
The LEGB rule
To resolve a name being read, Python searches four scopes in order, stopping at the first match:
- Local — names assigned inside the current function.
- Enclosing — names in any enclosing function (for nested functions).
- Global — names at the top level of the module.
- Built-in — names like
len,print,range.
x = "global"
def outer():
x = "enclosing"
def inner():
print(x) # finds "enclosing" — the nearest scope that has x
inner()
outer() # enclosing
If inner had its own x, that local would win. If no scope has the name, you get a
NameError.
Assignment decides the scope
Here's the rule that catches everyone: assigning to a name anywhere in a function makes that name local for the entire function — even before the assignment line runs.
count = 0
def increment():
count = count + 1 # UnboundLocalError!
return count
Because count is assigned in increment, Python treats it as a local throughout the
function. The right-hand side then tries to read a local that hasn't been assigned yet — so
it raises UnboundLocalError, not a NameError and not "use the global one".
The global keyword
To rebind a module-level name from inside a function, declare it global:
count = 0
def increment():
global count
count = count + 1 # now refers to the module-level count
return count
increment() # 1
increment() # 2
Note you only need global to assign. Reading a global needs no declaration —
mutating a global list (my_list.append(...)) also needs none, because that's mutation,
not rebinding.
The nonlocal keyword
nonlocal is the enclosing-scope equivalent: it lets a nested function rebind a variable
in the nearest enclosing function (not the module).
def make_counter():
count = 0
def increment():
nonlocal count # rebind the enclosing count
count += 1
return count
return increment
c = make_counter()
c(), c(), c() # (1, 2, 3)
Without nonlocal, count += 1 would make count local to increment and raise
UnboundLocalError. nonlocal requires an existing binding in an enclosing function — it
can't reach the global scope and it can't create a new variable.
The late-binding closure trap
Closures capture variables, not values — they look the variable up when called, not when defined. This bites in loops:
funcs = [lambda: i for i in range(3)]
[f() for f in funcs] # [2, 2, 2] — not [0, 1, 2]!
All three lambdas share the same i, which is 2 by the time they run. The fix is to bind
the current value with a default argument (evaluated at definition time):
funcs = [lambda i=i: i for i in range(3)]
[f() for f in funcs] # [0, 1, 2]
Comprehensions have their own scope
Since Python 3, comprehensions run in their own scope, so the loop variable doesn't leak into the surrounding function:
[y for y in range(3)]
y # NameError — y did not leak out
This is different from a plain for loop, where the loop variable does survive after the
loop ends.
Recap
Python resolves names with LEGB: Local, Enclosing, Global, Built-in, stopping at the
first match. Assignment anywhere in a function makes the name local for the whole
function, which is the source of UnboundLocalError. Use global to rebind a
module-level name and nonlocal to rebind an enclosing function's name — both are only
needed for assignment, not for reading or mutating. Remember that closures capture
variables (late binding), so capture per-iteration values with a default argument when you
build functions in a loop.