
Python Scope Explained: LEGB, global, and nonlocal
Every time Python sees a name like count or len, it has to figure out which variable that name refers to. Most of the time the answer is obvious and you never think about it. Then one day you add count += 1 inside a function and Python throws UnboundLocalError at a variable that is very clearly defined three lines above. Or a loop variable survives after the loop, while a comprehension variable doesn't.
All of this follows from a small set of scoping rules. Python decides where a name lives when it compiles the function, not when the line runs, and it searches a fixed sequence of scopes known as LEGB. Once you know those two facts, the confusing errors stop being confusing.
This post covers the four scopes, how Python decides a name is local, the UnboundLocalError trap, the global and nonlocal keywords and when they're appropriate, which blocks do and don't create a scope, and a few edge cases with classes and closures.
The Four Scopes: LEGB
When Python looks up a name, it searches these scopes in order and stops at the first match:
- Local: names assigned inside the current function.
- Enclosing: names in any enclosing function, from the innermost outward. This only exists for nested functions.
- Global: names assigned at the top level of the current module.
- Built-in: names Python provides everywhere, like
len,print,ValueError, andrange.
If the name isn't found in any of them, you get a NameError.
Here's all four in one example:
x = "global"
def outer():
x = "enclosing"
def inner():
x = "local"
print(x)
inner()
print(x)
outer()
print(x)
local
enclosing
global
Each function has its own x. Assigning to x inside inner() creates a new local variable; it doesn't touch the x in outer() or the global one. Each print(x) finds the nearest one.
Remove the local assignment and lookup continues outward:
def outer2():
msg = "from outer"
def inner():
print(msg)
inner()
outer2()
from outer
inner() has no local msg, so Python finds it in the enclosing scope. And built-ins are just the last stop on the same search, which is why len works anywhere:
import builtins
def show_len() -> int:
return len("abc")
print(show_len(), "len" in dir(builtins))
3 True
Shadowing Built-ins
Since built-ins are searched last, any local or global with the same name hides them:
def shadow():
list = [1, 2]
return list
print(shadow(), list("ab"))
[1, 2] ['a', 'b']
Inside shadow(), list is a local variable; outside, the built-in is untouched. It's harmless here, but shadowing list, id, type, input, or filter in a larger function means you can't call the built-in later in that function. Linters like Ruff flag this (the A rules from flake8-builtins), and it's worth turning that check on.
Python Decides "Local" at Compile Time
This is the rule that explains almost every scope surprise:
If a name is assigned anywhere in a function body, it is local to that entire function, unless declared
globalornonlocal.
"Assigned" includes more than =. Any of these make a name local:
x = ...,x += ..., and other augmented assignmentsfor x in ...import xandfrom m import xdef x(...)andclass x:with ... as xandexcept ... as x- Unpacking targets,
x := ..., andcasecapture patterns
Python scans the whole function body when compiling it. It doesn't matter whether the assignment comes before or after the line that reads the name, or whether it ever runs at all.
You can see the result of that decision on the function's code object:
def g():
a = 1
b = 2
return locals()
print(g())
print(g.__code__.co_varnames)
{'a': 1, 'b': 2}
('a', 'b')
co_varnames lists the local variable names that the compiler fixed in advance.
The UnboundLocalError Trap
Now the classic error makes sense:
count = 0
def increment():
count += 1
increment()
UnboundLocalError: cannot access local variable 'count' where it is not associated with a value
count += 1 is an assignment, so count is local to increment(). To compute count + 1, Python reads the local count, which hasn't been given a value yet. It never looks at the global count, because the compiler already decided the name is local.
The assignment doesn't even have to come first:
total = 10
def report():
print(total)
total = 0
report()
UnboundLocalError: cannot access local variable 'total' where it is not associated with a value
Without the total = 0 line at the bottom, print(total) would happily print 10. With it, total is local everywhere in the function, including the print line above it.
UnboundLocalError is a subclass of NameError. The message in Python 3.11+ is much clearer than the old "referenced before assignment" wording, and it points at exactly this situation: a local variable read before any value was bound to it.
There are two fixes, depending on what you meant:
- You meant to use a separate local variable. Rename it so it doesn't collide with the outer name.
- You meant to modify the outer variable. Declare it with
globalornonlocal, or better, restructure the code so the function returns a value instead.
The global Keyword
global name tells Python that, inside this function, name refers to the module-level variable. Reads and assignments both go to the global:
counter = 0
def bump() -> None:
global counter
counter += 1
bump()
bump()
print(counter)
2
global can even create a module-level variable that didn't exist before:
def f():
global created_here
created_here = "hi"
f()
print(created_here)
hi
That last example is also a good illustration of why global is usually discouraged. Code elsewhere in the module now depends on f() having been called first, and nothing in the source makes that dependency visible.
You Don't Need global to Mutate
global is only needed to rebind a global name. Calling a method that mutates a global object is just a read of the name, so no declaration is required:
items = []
def add(item):
items.append(item)
add(1)
print(items)
[1]
items.append() looks up items (finding the global) and mutates the list. Nothing is assigned to the name items, so it never becomes local. This distinction between rebinding a name and mutating an object comes up constantly; Python Variables and Mutability covers it in detail.
Alternatives to global
Module-level mutable state makes code harder to test and reason about. Common alternatives:
- Return values and let the caller decide what to store.
counter = bump(counter)is explicit. - Pass state in as an argument, or bundle it into an object with methods.
- Use a class when several functions share state.
- Keep true constants global (configuration, compiled regexes, lookup tables). Reading globals is fine; it's rebinding them from functions that causes trouble.
The nonlocal Keyword
nonlocal is the nested-function counterpart of global. It tells Python that a name refers to a variable in the nearest enclosing function scope, so you can rebind it:
def make_counter():
count = 0
def step() -> int:
nonlocal count
count += 1
return count
return step
c = make_counter()
print(c(), c(), c())
c2 = make_counter()
print(c2())
1 2 3
1
Each call to make_counter() creates a fresh count, and the returned step() function keeps updating its own copy. That's a closure: an inner function that remembers variables from the scope it was defined in. Without nonlocal, count += 1 would make count local to step() and you'd get the same UnboundLocalError as before:
def make_counter_bad():
count = 0
def step():
count += 1
return count
return step
make_counter_bad()()
UnboundLocalError: cannot access local variable 'count' where it is not associated with a value
nonlocal has stricter rules than global:
- The name must already exist in an enclosing function. It can't create a new variable, and it never refers to globals.
- It only works inside nested functions.
def f():
nonlocal zz
SyntaxError: no binding for nonlocal 'zz' found
Just as with global, mutating an enclosing object doesn't need nonlocal:
def outer4():
data = [1]
def inner():
data.append(2)
inner()
return data
print(outer4())
[1, 2]
Closures are the main reason nonlocal exists, and they're powerful enough to deserve their own post: see Closures in Python.
Closures Look Up Variables Late
Enclosing variables are looked up when the inner function runs, not when it's defined. The inner function sees the variable's current value:
def outer3():
v = 1
def inner():
return v
v = 2
return inner
print(outer3()())
2
The same rule means an inner function can refer to a name assigned later in the enclosing function, as long as it's assigned by the time the inner function is called:
def lookup():
def inner():
return later
later = "defined after inner"
return inner()
print(lookup())
defined after inner
Late binding is behind a well-known bug with functions created in a loop:
def make_multipliers():
return [lambda v: v * i for i in range(3)]
print([m(10) for m in make_multipliers()])
[20, 20, 20]
All three lambdas share the same i, and by the time they're called the loop has finished with i == 2. The standard fix is to bind the current value as a default argument, which is evaluated at definition time:
def make_multipliers2():
return [lambda v, i=i: v * i for i in range(3)]
print([m(10) for m in make_multipliers2()])
[0, 10, 20]
functools.partial() is a cleaner alternative when the function already exists.
What Does and Doesn't Create a Scope
Coming from languages with block scope, it's surprising how few constructs create a new scope in Python. Only modules, functions (including lambdas), classes, and comprehensions do. Blocks like if, for, while, with, and try don't:
for i in range(3):
pass
print(i)
if True:
inside_if = "visible"
print(inside_if)
2
visible
The loop variable survives with its last value, and a variable assigned inside an if is visible after it. That's by design; it means you can assign in a branch and use the value afterward.
There are two exceptions worth knowing.
Comprehension variables don't leak. A comprehension runs in its own scope (since Python 3.12 it's inlined for speed, but the isolation is preserved):
print([n for n in range(3)])
print(n)
[0, 1, 2]
NameError: name 'n' is not defined
Exception variables are deleted after the except block. Python implicitly runs del err at the end of the handler to avoid a reference cycle between the exception and the stack frame:
try:
1 / 0
except ZeroDivisionError as err:
pass
print(err)
NameError: name 'err' is not defined
If you need the exception later, assign it to another name inside the block: saved = err.
Class Bodies Are Not an Enclosing Scope
Class bodies have their own namespace, but it's not part of the LEGB chain for functions defined inside the class. Methods can't see class attributes as bare names:
class A:
y = 5
def method(self):
return y
def method2(self):
return self.y
print(A().method2())
print(A().method())
5
NameError: name 'y' is not defined
Inside method(), the lookup goes local, then (no enclosing function), then global, then built-in, and never checks the class body. You have to go through self.y or A.y. This is deliberate: it keeps attribute access explicit and makes inheritance work, since self.y can find an override on a subclass. The same rule causes the class-body comprehension gotcha described in List, Dict, and Set Comprehensions in Python.
Inspecting Scopes
A few tools help when you're debugging scope issues:
locals()returns the current local namespace as a dict. In a function, treat it as a snapshot; since Python 3.13 (PEP 667), each call returns an independent snapshot, and writing to it never changes the real local variables.globals()returns the module's global namespace as a live dict.func.__code__.co_varnameslists a function's local names;co_freevarslists the enclosing variables a closure uses.
def closure_vars():
a = 1
def inner():
return a
return inner
print("x" in globals())
print(closure_vars().__code__.co_freevars)
True
('a',)
(Here x is the global from the first example in the same script.) You rarely need these in application code, but they're useful for understanding what the compiler decided.
Quick Reference
| Situation | What happens | Fix |
|---|---|---|
| Read an outer variable | Found through LEGB | Nothing needed |
| Assign to a name in a function | Creates a local | Intended in most cases |
| Read then assign the same name | UnboundLocalError | Rename, global/nonlocal, or return a value |
Mutate an outer object (.append()) | Works, no declaration | Nothing needed |
| Rebind a module variable | Needs global | Prefer returning values |
| Rebind an enclosing function's variable | Needs nonlocal | Common in closures |
| Use a class attribute in a method | NameError | Use self.attr or ClassName.attr |
Variable from if/for block | Still visible after | By design |
Conclusion
Python resolves names through four scopes, local, enclosing, global, and built-in, and decides at compile time which names are local to each function: anything assigned anywhere in the body. That single rule explains UnboundLocalError, why mutating an outer list works while rebinding it doesn't, and why moving one assignment can change how an earlier line behaves.
Use global and nonlocal when you genuinely need to rebind an outer name, with nonlocal mostly reserved for closures and global best kept rare. Remember that blocks don't create scopes, comprehensions and except ... as variables are the exceptions, and class bodies aren't visible from methods. With LEGB in mind, you can predict where any name will be found before you run the code.


