1.2.30 · D4Introduction to Programming (Python)

Exercises — Variable scope — LEGB rule (Local, Enclosing, Global, Built-in)

2,422 words11 min readBack to topic

Before we start, one picture of the whole game, so every later trace has a map to point at.

Figure — Variable scope — LEGB rule (Local, Enclosing, Global, Built-in)

Level 1 — Recognition

(Can you name what you see?)

Exercise 1.1

In the code below, label each name (total, add, print, n) with the scope Python finds it in.

total = 10                # line A
def add(n):               # line B
    print(total + n)      # line C
add(5)
Recall Solution 1.1
  • total on line C: not assigned inside add, so it is not Local. Search continues → Global (assigned at top level, line A). Scope = G.
  • n on line C: it is the parameter of add. Parameters are assigned when the function is called, so n is Local. Scope = L.
  • print on line C: not L, not E (no enclosing function), not G → found in Built-in. Scope = B.
  • add on line B: assigned (via def) at top level → Global.

Output: add(5) prints total + n = 10 + 5 = 15.

Exercise 1.2

Which of these create a new scope in Python? for loop · function body · if block · module (file) · list comprehension.

Recall Solution 1.2

New scope: function body, module, list comprehension. No new scope: for loop, if block. Names assigned inside a for/if live in the surrounding function/module — they leak out.


Level 2 — Application

(Apply the READ order and predict output.)

Exercise 2.1

x = "G"
def outer():
    x = "E"
    def inner():
        x = "L"
        return x
    return inner()
print(outer())

What prints, and which room won?

Recall Solution 2.1

Inside inner, x is assigned (x = "L"), so x is Local to inner. Reading x in return x finds L first → "L". Output: L.

Exercise 2.2

Delete the line x = "L" from Exercise 2.1 so inner becomes:

def inner():
    return x

Now what prints?

Recall Solution 2.2

inner no longer assigns x, so x is not Local. Search order: L (miss) → E = outer's locals, which has x = "E" → hit. Output: E. Python never reaches G.

Exercise 2.3

len = 5
print(len("hi"))

Predict the result.

Recall Solution 2.3

At module level len = 5 creates a Global name len. When len("hi") reads len, LEGB checks L (miss) → E (none) → G (len = 5, found!) → stops before Built-in. So len is the integer 5, and 5("hi") tries to call an integerTypeError: 'int' object is not callable. This is shadowing the built-in.


Level 3 — Analysis

(Diagnose why code errors, using the WRITE rule.)

Exercise 3.1

count = 0
def bump():
    count = count + 1
    return count
bump()

This raises an error. Name it and explain the exact line and reason.

Recall Solution 3.1

Error: UnboundLocalError: local variable 'count' referenced before assignment, on the line count = count + 1. Reason: count appears on the left of = inside bump, so by the WRITE rule count is Local for the whole function. Evaluating the right side count + 1 must read the local count — but it hasn't been assigned yet. Python does not fall back to the global count; the WRITE rule already decided count is Local.

Exercise 3.2

def outer():
    def inner():
        nonlocal y
        y = 1
    inner()
outer()

Does this run? If not, why?

Recall Solution 3.2

It errors at compile/def time: SyntaxError: no binding for nonlocal 'y' found. Reason: nonlocal y demands that y already exists in an enclosing function (E). But outer never assigns y. There is no enclosing y to bind to, so the nonlocal statement is illegal. Fix: add y = 0 inside outer (before inner), giving nonlocal y a target.

Exercise 3.3

x = 10
def f():
    print(x)
    x = 20
f()

Trace carefully. What happens on the print(x) line?

Recall Solution 3.3

Because x = 20 appears later in f, the WRITE rule marks x Local for the whole function — including the earlier print(x). So print(x) tries to read the local x before it was assigned → UnboundLocalError. The trap: the assignment is below the print, yet it still poisons the print above it. Scope is decided for the entire function body at once, not line by line.


Level 4 — Synthesis

(Combine global, nonlocal, and closures to build working code.)

Exercise 4.1

Fix Exercise 3.1 so bump() actually increments the module-level count. Then call it 3 times and give the final count.

Recall Solution 4.1
count = 0
def bump():
    global count
    count = count + 1
    return count
bump(); bump(); bump()
print(count)   # 3

global count tells Python "skip the Local-creation rule; count here means the module-level name." Each call reads then rebinds the global. After 3 calls, count == 3.

Exercise 4.2

Build a counter closure: a function make_counter() that returns a function which, each time it is called, returns 1, 2, 3, … Use nonlocal.

Recall Solution 4.2
def make_counter():
    n = 0                 # lives in Enclosing scope
    def step():
        nonlocal n        # bind to outer n, not a new local
        n += 1
        return n
    return step
 
c = make_counter()
print(c(), c(), c())      # 1 2 3

n lives in make_counter's scope (E). Inside step, n += 1 is a write, so without nonlocal it would create a fresh local and error (n referenced before assignment). nonlocal n points it at the enclosing n, which survives between calls because the returned step keeps that enclosing scope alive — that is a closure.

Exercise 4.3

Two independent counters from the same factory:

a = make_counter()
b = make_counter()
print(a(), a(), b(), a())   # ?
Recall Solution 4.3

Output: 1 2 1 3. Each make_counter() call creates its own enclosing scope with its own n. a and b do not share n. So a counts 1,2,…,3 while b independently starts at 1.


Level 5 — Mastery

(Full-trace problems where several rules interact — predict exact output.)

Exercise 5.1

x = "G"
def outer():
    x = "E"
    def inner():
        global x
        x = "changed"
    inner()
    return x
r = outer()
print(r, x)

Predict the two printed values.

Recall Solution 5.1

Inside inner, global x targets the module x, NOT outer's x. So x = "changed" rebinds the global. outer's local x ("E") is untouched.

  • return x inside outer reads outer's local x = "E", so r == "E".
  • The global x is now "changed". Output: E changed.

Exercise 5.2

def f():
    total = 0
    for i in range(3):
        total += i
    return total, i
print(f())

Does i exist after the loop? What is the output?

Recall Solution 5.2

A for loop does not create scope. i is assigned in f's body, so i is Local to f and survives after the loop. Its last value is 2 (range(3) → 0,1,2). total = 0+0+1+2 = 3. Output: (3, 2).

Exercise 5.3

The comprehension scope subtlety:

x = "G"
squares = [x for x in range(3)]
print(x)

What prints, and why is this different from the for loop above?

Recall Solution 5.3

A list comprehension has its own scope (unlike a for statement). The comprehension's x is a separate Local that does not leak out. So the module-level x is never touched. Output: G. Contrast with 5.2: a plain for leaks i; a comprehension's loop variable does not leak. Same-looking loops, different scope rules.

Exercise 5.4

Grand finale — combine everything:

val = "global"
def a():
    val = "enclosing"
    def b():
        def c():
            return val
        return c()
    return b()
print(a())

Which room does c read val from?

Recall Solution 5.4

c does not assign val, so it reads outward: L (miss) → E. The enclosing chain of c is b, then a. b has no val; a has val = "enclosing". That is the first E-room with the name → hit. Output: enclosing. Python stops before Global.


Active Recall

The WRITE rule marks a name Local when
the name appears on the left of =, in +=, as a for target, def, or import — anywhere in the function, unless global/nonlocal is used.
Reading a name searches rooms in the order
Local → Enclosing → Global → Built-in (first hit wins).
print(x) before x = 20 in a function raises
UnboundLocalError, because the later assignment makes x Local for the whole function.
nonlocal y with no enclosing y gives
a SyntaxError (no binding for nonlocal found).
A plain for i in range(3) after the loop leaves i equal to
2 — the loop does not create scope, so i leaks and keeps its last value.
A list comprehension's loop variable
has its own scope and does NOT leak into the surrounding function/module.
global x inside a nested function targets
the module-level x (G), skipping all enclosing functions.
Two calls to a make_counter() factory share their n?
No — each call gets its own enclosing scope, so the counters are independent.

Connections

  • Functions and Parameters — every exercise's scope is created by a function call; parameters are Local names.
  • NameError and UnboundLocalError — Levels 3 and 5 are pure diagnosis of these two errors.
  • Closures and Nested Functions — Exercises 4.2–4.3 and 5.4 are closures over the Enclosing scope.
  • Namespaces and the dict model — each "room" is a namespace dict behind the scenes.
  • Mutable vs Immutable arguments — a companion trap: rebinding vs mutating.
  • Modules and import — the Global room is the module namespace populated by import.