Scope and the LEGB Rule
Python resolves a name by looking in four places in a fixed order: local, enclosing, global, built-in. Assignment anywhere in a function makes the name local for the whole function.
- Basics
- Data Types
- Operators
- Strings
- Control Flow
- Lists
- Tuples
- Sets
- Dictionaries
- Comprehensions
- Functions
- Advanced Functions
- Recursion
- Exception Handling
- File Handling
- Modules
- Standard Library
- OOP
- Advanced OOP
- Iterators and Generators
- Decorators
- Context Managers
- Descriptors and Dataclasses
- Python Internals
- Concurrency
- Regular Expressions
- Serialization
- Command Line Python
- Testing and Debugging
- Type Hints
- Performance
- Python Security
- DSA with Python
The four scopes
B Built-in names Python always provides: print, len, str, ...
G Global names at the top level of the module
E Enclosing names in an outer function, for a nested function
L Local names inside the current function
Lookup order: L -> E -> G -> B (the first match wins)value = "global"
def outer():
value = "enclosing"
def inner():
value = "local"
print(value) # local
inner()
print(value) # enclosing
outer()
print(value) # globalThree separate variables happen to share a name. Each print finds the nearest one.
Reading is easy, writing is not
count = 10
def show():
print(count) # 10 - reading a global works without ceremony
show()count = 10
def increment():
count = count + 1 # UnboundLocalError
print(count)
# increment()This is the single most confusing scope error in Python, and the reason is worth stating precisely:
If a name is assigned anywhere in a function, Python treats it as local for the whole function, including lines before the assignment.count = count + 1therefore tries to read a localcountthat has not been assigned yet.
global
count = 10
def increment():
global count
count = count + 1
increment()
print(count) # 11global tells Python that assignments to this name in this function refer to the module level variable. It works, and it is almost always the wrong design. Prefer returning a value:
count = 10
def increment(current):
return current + 1
count = increment(count)
print(count) # 11The second version can be tested, called twice safely, and reasoned about without reading the rest of the file.
nonlocal
def counter():
count = 0
def increment():
nonlocal count # the count in the ENCLOSING function
count += 1
return count
return increment
tick = counter()
print(tick(), tick(), tick()) # 1 2 3nonlocal targets the nearest enclosing function scope, never the global one. Without it, count += 1 raises UnboundLocalError for exactly the reason above. This pattern is a closure, covered fully in the advanced functions note.
global | nonlocal | |
|---|---|---|
| Targets | Module level | Nearest enclosing function |
| Name must already exist | No | Yes |
| Typical use | Rarely justified | Closures and counters |
Mutation does not need a declaration
items = []
settings = {"a": 1}
def add():
items.append(1) # MUTATING - no global needed
settings["b"] = 2
def replace():
global items
items = [9] # REBINDING - global is required
add()
print(items, settings) # [1] {'a': 1, 'b': 2}The rule is about binding a name, not about changing an object. items.append(...) never rebinds items, so no declaration is needed. That is also why mutable global state is easy to change by accident.
The built-in scope
print(len("abc")) # len comes from the built-in scope
len = 5 # a global named len now shadows it
# print(len("abc")) # TypeError: 'int' object is not callable
del len # remove the shadow, the built-in is visible again
print(len("abc")) # 3Shadowing list, dict, str, id, type, sum, max, min, input and next is easy to do and produces errors that look impossible. If a built in stops working, search the file for an assignment to its name.
Scopes that do not exist
if True:
inside_if = "visible"
for i in range(3):
pass
while False:
pass
print(inside_if) # visible - if does not create a scope
print(i) # 2 - the loop variable survivesOnly functions, classes, modules and comprehensions create scopes. Blocks do not. A name assigned inside an if or a for is visible afterwards, which is convenient and occasionally surprising.
n = "outer"
squares = [n * n for n in range(3)]
print(n) # outer - a comprehension DOES have its own scopeInspecting the scopes
value = "global value"
def demo():
local_value = "local value"
print(sorted(locals())) # ['local_value']
print("value" in globals()) # True
demo()A worked example
TAX_RATE = 0.18 # a module level constant: acceptable global state
def line_total(price, quantity):
subtotal = price * quantity # local
return subtotal * (1 + TAX_RATE) # reads the global constant
def invoice_total(lines):
total = 0 # local to this function
for price, quantity in lines:
total += line_total(price, quantity)
return total
print(round(invoice_total([(100, 2), (50, 3)]), 2))Constants read from the global scope are fine. Mutable state written from several functions is what causes trouble.
Common mistakes
- Assigning to a global inside a function without declaring it, then meeting
UnboundLocalError. - Using
globalas the first solution instead of returning a value. - Using
globalwherenonlocalwas needed inside a nested function. - Shadowing a built in name.
- Expecting
ifandforblocks to create scopes. - Relying on the loop variable after the loop without noticing it is still bound.
Best practices
- Pass values in as parameters and hand results back with
return. - Keep globals to constants, written in capitals.
- Use
nonlocalonly inside genuine closures. - Never name a variable after a built in.
- Keep functions short enough that every local name is visible at once.
Practice
- Explain the exact reason
count = count + 1raisesUnboundLocalErrorwhencountis global. - Write a counter using
nonlocal, then rewrite it without any declaration at all. - Demonstrate a case where a function changes a global list without using
global, and explain why. - Shadow
listdeliberately, observe the failure, and recover in the same session. - Predict what
nholds after aforloop and after a comprehension over the same range.
Conclusion
Names resolve local, enclosing, global, built-in. Assignment anywhere in a function makes a name local everywhere in that function - that one rule explains every scope error you will meet. Prefer parameters and return values to global.