Scope: global, nonlocal & LEGB
Reviewed & published by Brayan K
Scope is the set of rules that decides which variable a name refers to, and Python resolves it with the LEGB rule — searching Local, then Enclosing, then Global, then Built-in scopes and using the first match it finds.
Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
Understanding scope is the difference between confidently sharing state and being baffled by a mysterious UnboundLocalError. Once you know how Python looks up names, the global and nonlocal keywords stop feeling like magic.
What You'll Learn in This Lesson
- The LEGB lookup order, scope by scope
- Reassigning module variables with global
- Updating enclosing state with nonlocal
- Building stateful closures with nonlocal
- Why reading-then-assigning triggers UnboundLocalError
- Why mutating differs from rebinding a variable
1 The LEGB Lookup Order
When Python sees a name, it searches four scopes in order and stops at the first match: Local → Enclosing → Global → Built-in. Here all four hold an x, and each scope sees the closest one:
x = "global" # Global scope
def outer():
x = "enclosing" # Enclosing scope
def inner():
x = "local" # Local scope
print("inner sees:", x)
inner()
print("outer sees:", x)
outer()
print("module sees:", x)
# inner sees: local
# outer sees: enclosing
# module sees: global📐 The search path for a name:
Local -> Enclosing -> Global -> Built-in
(this (outer (module (len, print,
func) funcs) top) range, ...)This is why you can call len() anywhere — it's found in the Built-in scope, the last stop on the search.
🌍 The global Keyword
Reading a global needs nothing special. But reassigning one from inside a function requires the global keyword — otherwise the assignment quietly creates a new local instead:
counter = 0
def increment():
global counter # "I mean the module-level counter"
counter += 1
increment()
increment()
increment()
print("counter:", counter) # counter: 3
# Reading a global needs NO keyword:
def report():
print("current:", counter) # just reads it fine
report() # current: 3Without global counter, the line counter += 1 would try to read a local counter that doesn't exist yet — and crash. The keyword says "operate on the outer one."
2 nonlocal & Stateful Closures
nonlocal is global's cousin for enclosing scopes. It lets an inner function reassign a variable in the function that wraps it — the foundation of closures that remember state between calls:
def make_counter():
count = 0 # lives in the enclosing scope
def step():
nonlocal count # "update the enclosing count"
count += 1
return count
return step
c = make_counter()
print(c()) # 1
print(c()) # 2
print(c()) # 3 <- state persists between callsdef make_account(balance):
def deposit(amount):
nonlocal balance
balance += amount
return balance
def withdraw(amount):
nonlocal balance
balance -= amount
return balance
return deposit, withdraw
deposit, withdraw = make_account(100)
print("After deposit:", deposit(50))
print("After withdraw:", withdraw(30))
print("After deposit:", deposit(10))
# ✅ Expected output:
# After deposit: 150
# After withdraw: 120
# After deposit: 1303 The UnboundLocalError Trap
This one bites everyone. If a function assigns to a name anywhere in its body, Python marks that name local for the whole function — even on lines before the assignment. Reading it first then fails:
total = 100
def broken():
print(total) # tries to read LOCAL total (not yet set)
total = total + 1 # this assignment makes 'total' local
try:
broken()
except UnboundLocalError as e:
print("UnboundLocalError:", e)
# UnboundLocalError: cannot access local variable 'total'
# where it is not associated with a value🔀 Mutating vs Rebinding
Why can you list.append() to a global list without the global keyword, but can't reassign it? Because mutating an object and rebinding a name are different operations:
items = [1, 2, 3]
def mutate():
items.append(4) # MUTATES the existing list object
# (no assignment to 'items' -> no local)
mutate()
print("mutated:", items) # mutated: [1, 2, 3, 4]
config = {"v": 1}
def rebind():
config = {"v": 2} # REBINDS a new local 'config'
# the global is untouched
rebind()
print("after rebind:", config) # after rebind: {'v': 1}🎯 Your Turn: Fill in the Blank
Fix this counter closure so it actually increments. Replace each ___, then run it.
# 🎯 YOUR TURN: replace each ___ so the counter increments
def make_ticker():
n = 0
def tick():
___ n # keyword to update the enclosing n
n ___ 1 # operator to add 1 to n
return n
return tick
t = make_ticker()
print(t(), t(), t())
# Expected output:
# 1 2 3def make_ticker():
n = 0
def tick():
nonlocal n
n += 1
return n
return tick
t = make_ticker()
print(t(), t(), t()) # 1 2 3Common Errors
❌ Forgetting global when reassigning
count = 0
def bump():
count += 1 # UnboundLocalError: count is local here✅ Declare it global before reassigning:
def bump():
global count
count += 1❌ Using global where you needed nonlocal
def outer():
x = 0
def inner():
global x # looks for a MODULE x, not outer's x!
x += 1✅ To touch an enclosing function's variable, use nonlocal x, not global x.
❌ Shadowing a built-in by accident
list = [1, 2, 3] # 'list' is now your variable...
nums = list(range(3)) # TypeError: 'list' object is not callable✅ Don't name variables after built-ins (list, dict, str, id, sum). A local name shadows the built-in for that scope.
🎯 Mini Challenge: A Running Average
Build a closure make_averager() that remembers every number it's given and returns the running average. Use nonlocal to track the total and count.
def make_averager():
total = 0
count = 0
def add(value):
nonlocal total, count
total += value
count += 1
return total / count
return add
avg = make_averager()
print(avg(10)) # 10.0
print(avg(20)) # 15.0
print(avg(30)) # 20.0
print(avg(0)) # 15.0
# ✅ Expected output:
# 10.0
# 15.0
# 20.0
# 15.0📚 Keep Learning
- Naming and binding (Execution model) — the precise rules behind LEGB and local-name binding.
- The global statement — and the nonlocal statement reference.
- FAQ: rules for local and global variables — a friendly explanation of the assignment-makes-it-local rule.
Frequently Asked Questions
Lesson complete — scope holds no mystery now!
You can trace any name through the LEGB order, reassign globals with global, build stateful closures with nonlocal, diagnose the UnboundLocalError trap, and tell mutation apart from rebinding. That's a superpower for reading and debugging real code.
Practice quiz
What does the LEGB rule stand for, in search order?
- Loop, Else, Generator, Block
- Late, Early, Global, Bound
- Local, Enclosing, Global, Built-in
- Local, External, Global, Base
Answer: Local, Enclosing, Global, Built-in. Python resolves a name by searching Local, then Enclosing, then Global, then Built-in scopes.
Given x='global', an outer with x='enclosing', and inner with x='local', what does inner print for x?
- local
- global
- enclosing
- UnboundLocalError
Answer: local. LEGB stops at the first match — inner's own Local x wins.
When do you need the global keyword inside a function?
- To read a module-level variable
- To call a built-in function
- Never
- To reassign (rebind) a module-level variable
Answer: To reassign (rebind) a module-level variable. Reading a global needs nothing; rebinding one from inside a function requires 'global'.
What does nonlocal target?
- The module/global scope
- The nearest enclosing function's scope
- The built-in scope
- A brand-new local
Answer: The nearest enclosing function's scope. nonlocal lets an inner function reassign a variable in the function that wraps it.
A make_counter closure uses 'nonlocal count; count += 1'. What do three calls print?
- 1 2 3
- 1 1 1
- 0 1 2
- 3 3 3
Answer: 1 2 3. nonlocal makes the count persist and increment across calls, giving 1, 2, 3.
Why does reading a name then assigning it later in the same function raise UnboundLocalError?
- The name is misspelled
- Python forbids reassignment
- An assignment anywhere makes the name local for the WHOLE function
- The global was deleted
Answer: An assignment anywhere makes the name local for the WHOLE function. Because assignment anywhere marks the name local everywhere, the earlier read hits an uninitialized local.
items=[1,2,3] is global. A function calls items.append(4) with no 'global'. What is items afterward?
- [1, 2, 3]
- [1, 2, 3, 4]
- [4]
- UnboundLocalError
Answer: [1, 2, 3, 4]. append MUTATES the existing object — no assignment, no local — so the global list is changed.
config={'v':1} is global. A function does config={'v':2} with no 'global'. What is the global config after?
- {'v': 2}
- {}
- UnboundLocalError
- {'v': 1}
Answer: {'v': 1}. Rebinding creates a NEW local config; the global is untouched, staying {'v': 1}.
To update an enclosing function's variable from an inner function, you use...
- global
- nonlocal
- static
- extern
Answer: nonlocal. nonlocal targets the enclosing function; global would wrongly look for a module-level name.
Why can you call len() from any scope without importing it?
- It is a keyword
- It is automatically local
- It lives in the Built-in scope, the last LEGB stop
- It is enclosing-scoped
Answer: It lives in the Built-in scope, the last LEGB stop. Built-ins like len, print, and range are found in the B scope, searched last in LEGB.
Continue this course
- Previous: __slots__ & Memory Optimization
- Next: Custom Exceptions & Exception Chaining — Build exception hierarchies and chain causes with raise ... from
- Quick reference: Python cheat sheet
- From the blog: The Two Things That Actually Confused Me Learning Python (That No Course Explains)
Frequently asked questions
What is the LEGB rule?
LEGB describes the order Python searches for a name: Local (inside the current function), then Enclosing (any outer functions that wrap it), then Global (the module's top level), then Built-in (names like len and print). Python uses the first match it finds, searching outward from the innermost scope.
When do I need the global keyword?
Only when you want to reassign a module-level variable from inside a function. Reading a global needs no keyword; rebinding one does. Without 'global x', any assignment to x inside the function creates a brand-new local variable instead of touching the global. In practice, prefer returning values over mutating globals.
What does nonlocal do, and how is it different from global?
nonlocal lets an inner function reassign a variable in its nearest enclosing function (not the global scope). global targets module level; nonlocal targets the enclosing function level. nonlocal is the key to writing closures that remember and update state between calls.
Why do I get UnboundLocalError when I try to read a variable that clearly exists?
Because if a function assigns to a name anywhere in its body, Python treats that name as local for the entire function — even on lines before the assignment. So reading it first hits an uninitialized local, not the global, and raises UnboundLocalError. Declaring 'global' or 'nonlocal' (or not reassigning) fixes it.