Exercises — Variable scope — LEGB rule (Local, Enclosing, Global, Built-in)
Before we start, one picture of the whole game, so every later trace has a map to point at.

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
totalon line C: not assigned insideadd, so it is not Local. Search continues → Global (assigned at top level, line A). Scope = G.non line C: it is the parameter ofadd. Parameters are assigned when the function is called, sonis Local. Scope = L.printon line C: not L, not E (no enclosing function), not G → found in Built-in. Scope = B.addon line B: assigned (viadef) 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 xNow 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 integer → TypeError: '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) # 3global 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 3n 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 xinsideouterreadsouter's localx="E", sor == "E".- The global
xis 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
=, 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
print(x) before x = 20 in a function raises
x Local for the whole function.nonlocal y with no enclosing y gives
A plain for i in range(3) after the loop leaves i equal to
i leaks and keeps its last value.A list comprehension's loop variable
global x inside a nested function targets
Two calls to a make_counter() factory share their n?
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.