Python variable scope and the LEGB rule
Quick answer: Python looks up a name in this order: Local, Enclosing, Global, then Built-in, which forms LEGB. Assignment inside a function normally creates a local name. Prefer parameters and return values; use global for intentional module state and nonlocal when a nested function must update an enclosing function's state.
This rule explains most NameError and UnboundLocalError exceptions. Scope is not merely where a variable “exists.” It determines which binding between a name and an object a statement will use. If function fundamentals are still unfamiliar, start with the guide to Python functions.
Names, objects, and scopes
In Python, a variable is a name bound to an object. quantity = 3 creates or replaces the quantity binding in the current namespace. Multiple names may reference one object, and rebinding one name does not automatically change that object.
A namespace maps names to objects. A scope is a code region where a namespace can be accessed directly. Modules, functions, and classes create namespaces, although their lookup behavior differs. The official scopes and namespaces tutorial describes this model in detail.
tax_rate = 0.10
def total_price(price):
discount = 5
return price * (1 + tax_rate) - discount
print(total_price(100)) # 105.0
During the call, price and discount are local while tax_rate comes from the module. Reading it requires no global declaration. Clear ownership becomes especially useful as code is split into modules and packages.
How LEGB lookup works
Python resolves a name through four levels:
- Local (L): parameters and bindings in the current function.
- Enclosing (E): scopes of outer functions surrounding the current one.
- Global (G): the namespace of the module where the function was defined.
- Built-in (B): names from
builtins, includinglen,sum, andprint.
label = "global"
def outer():
label = "enclosing"
def inner():
label = "local"
return label
return inner()
print(outer()) # local
Remove the assignment in inner and the result becomes enclosing. Remove the outer assignment as well and lookup reaches the global label. If all levels fail, Python raises NameError. See the Python execution model for the precise rules.
Built-in shadowing is legal but risky:
## Avoid: list no longer refers to the built-in constructor.
list = [1, 2, 3]
## Prefer a descriptive name.
numbers = [1, 2, 3]
The same warning applies to str, id, filter, and input. Shadowing can make a later call fail and forces readers to remember a surprising local meaning.
Local scope and UnboundLocalError
Python classifies local names while compiling the entire function body, not while executing individual lines. If a name is assigned anywhere in a function, it is local unless declared global or nonlocal.
counter = 10
def increment():
print(counter)
counter += 1
increment() # UnboundLocalError
Because counter += 1 reads and assigns, counter is local. The earlier print therefore reads a local binding before it exists. An explicit design is usually better:
def increment(counter):
return counter + 1
counter = increment(counter)
Parameters expose dependencies and simplify testing. This remains valuable in functional code and in object-oriented Python.
Using global deliberately
global directs assignments in a function to the module namespace. It applies to the whole block and must appear before that name is used in the same block.
calls = 0
def record_call():
global calls
calls += 1
This can be reasonable for small module configuration or tightly controlled instrumentation. Mutable globals nevertheless couple behavior to call order and make isolated tests harder. Returning a new value, storing state in an object, or injecting a dependency is often clearer.
global does not create a universal program-wide variable. It addresses the global namespace of the module containing the function. The global statement reference defines its compile-time behavior.
Closures and nonlocal
A nested function may retain bindings from an enclosing function after the outer call has returned. This is a closure; the guide to Python closures covers the pattern further.
def make_counter(start=0):
value = start
def next_value():
nonlocal value
value += 1
return value
return next_value
counter = make_counter(10)
print(counter()) # 11
print(counter()) # 12
Without nonlocal, assignment would make value local to next_value, and += would raise UnboundLocalError. nonlocal searches enclosing function scopes, not the module, and requires an existing binding.
Closures suit small private state, function factories, and decorators. The Python decorators guide shows a common application. When state has numerous operations or invariants, a class generally communicates the design better.
Classes, comprehensions, and subtle boundaries
A class body has its own namespace, but a method does not treat that namespace as an enclosing function scope. Access attributes through self, the class, or a class method:
class Cart:
limit = 20
def accepts(self, quantity):
return quantity <= self.limit
Using bare limit in the method does not automatically find Cart.limit. Methods start LEGB from their local function scope and then continue toward the defining module.
Comprehensions in Python 3 also have their own scope, so their iteration name does not leak:
squares = [number ** 2 for number in range(4)]
## number is not defined here
Closures built in loops introduce late binding because names are looked up when the function runs:
functions = [lambda n=n: n * 2 for n in range(3)]
print([function() for function in functions]) # [0, 2, 4]
The default argument captures each iteration's value. Without n=n, every lambda would observe the final value. Read lambda, map, and filter for more practical context.
Common mistakes and diagnosis
Confusing mutation with assignment: a function can mutate a globally referenced list without global, because it does not rebind the name. It is still a side effect and should be intentional.
items = []
def add(item):
items.append(item)
Applying global to every scope error: it may silence the exception while preserving hidden coupling. First consider a parameter, return value, or object attribute.
Expecting block scope: if, for, while, with, and try do not create a separate local scope. Their assignments remain in the current function or module, although a conditional branch may never create the name.
Debugging by guesswork: locals() and globals() reveal current namespaces and are useful for learning and diagnosis. Do not mutate their returned mappings as ordinary application design; make data flow explicit instead.
Final checklist
- Locate the function or module where each name is assigned.
- Apply LEGB and stop at the first existing binding.
- Prefer parameters and returns over mutable global state.
- Reserve
nonlocalfor simple closure state. - Access class attributes explicitly through
selfor the class. - Avoid shadowing built-ins such as
listandstr. - For
UnboundLocalError, search for assignment later in the same function.
LEGB becomes predictable once names, objects, and namespaces are treated separately. Expose dependencies in function signatures, and regard global and nonlocal as focused tools rather than shortcuts. That produces code that is easier to read, test, and change.