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

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: 3

Without 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 calls
def 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: 130

3 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 3
def make_ticker():
    n = 0
    def tick():
        nonlocal n
        n += 1
        return n
    return tick

t = make_ticker()
print(t(), t(), t())   # 1 2 3

Common 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

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

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.