← Home

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 += 1

But 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, testable

Rohan 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 inc

Tests

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

Resources