095. Global And Local Scope
Understand where variables live and when to use global
095. Global And Local Scope
Rohan adds a module-level alert counter. Every time RAM exceeds 90%, he wants alert_count to go up. He writes:
alert_count = 0
def check_ram(percent):
if percent >= 90:
alert_count += 1 # UnboundLocalError!Python crashes with UnboundLocalError: local variable 'alert_count' referenced before assignment. He’s confused — the variable is right there at the top of the file.
The problem: the moment Python sees alert_count += 1 inside the function, it decides alert_count is a local variable. Then it tries to read it (the left side of +=) before assigning it. Reading an unset local → crash.
The fix:
alert_count = 0
def check_ram(percent):
global alert_count # "I mean the module-level one"
if percent >= 90:
alert_count += 1But there’s a better way. Global mutable state is hard to test and reason about. Senior devs prefer returning values:
def check_ram(percent, count):
if percent >= 90:
return count + 1
return count
alert_count = 0
alert_count = check_ram(92, alert_count) # pure, testableRohan also builds a make_threshold_checker(threshold) factory — an inner function that captures threshold from the enclosing scope. The inner function can read the outer variable fine, but can’t reassign it without nonlocal.
💡 Fun fact: Python’s LEGB scope rule (Local → Enclosing → Global → Built-in) was formalized in Python 2.1 when nested scopes were introduced. Before that, Python 2.0 and earlier did not support proper closures — inner functions couldn’t see variables from enclosing functions at all. The fix required adding nonlocal semantics to the language’s compile step.
⚠️ Watch out: The UnboundLocalError trap is one of Python’s most confusing errors for beginners. The moment Python’s compiler sees any assignment to a name anywhere in a function, it marks that name as local for the entire function — including lines before the assignment. So x = x + 1 makes x local, and the x on the right side reads an undefined local, not the global.
🤔 Think about it: The scenario shows two approaches: global alert_count and the pure-function alternative return count + 1. The pure function is easier to test — why? What makes global mutable state hard to test, and what makes the return-value approach easier to reason about in a multithreaded monitoring tool?
Learning objectives
- Use global to modify module-level variables inside functions
- Understand why assignment creates a local variable by default (UnboundLocalError)
- Use a mutable container or nonlocal to modify enclosing scope
- Know the LEGB lookup order
Key concepts
- global
- local scope
- LEGB rule
- closure
- nonlocal
Try it
Concept detail
Python scope rules — LEGB lookup order: Local → variables assigned inside the current function Enclosing → variables in enclosing functions (for closures) Global → variables at module level Built-in → names like len, print, range
Reading globals works without declaration: x = 10 def f(): print(x) # fine — reads global x
Assigning to a global requires declaration: x = 10 def f(): global x x = 20 # without ‘global x’, this creates a LOCAL x and the global stays 10
The UnboundLocalError trap: x = 10 def f(): x += 1 # Python sees assignment → treats x as local → reads undefined local → crash Add ‘global x’ to fix.
nonlocal — for enclosing scope (not module): def outer(): count = 0 def inner(): nonlocal count # modify outer’s count, not a new local count += 1 inner() return count
Mutable container workaround (avoid nonlocal): count = [0] def inc(): count[0] += 1 # mutates the list, does not rebind count Works because you’re not reassigning count — you’re modifying what it points to.
Best practice: avoid global state. Use return values and parameters instead. Global is acceptable for module-level constants (ALL_CAPS) and config singletons.
Solution
counter = 0
def increment():
global counter
counter = counter + 1
def reset():
global counter
counter = 0
def get_count():
return counter
def make_counter(start=0):
count = [start]
def inc():
count[0] += 1
return count[0]
return incTests
def test_increment():
global counter
counter = 0
increment()
assert get_count() == 1
def test_increment_twice():
global counter
counter = 0
increment()
increment()
assert get_count() == 2
def test_reset():
global counter
counter = 5
reset()
assert get_count() == 0
def test_make_counter():
c = make_counter(0)
assert c() == 1
assert c() == 2
assert c() == 3
def test_make_counter_start():
c = make_counter(10)
assert c() == 11