Exercises — global and nonlocal keywords

Before you start, re-anchor the two rules that decide everything:
Work each one on paper FIRST, then open the solution. That is the point of a self-test page.
Level 1 — Recognition
(Can you spot which scope a name lives in, and whether a keyword is even needed?)
L1.1
For each line, say whether global/nonlocal is needed or not needed, and why.
total = 0
def a(): print(total) # (i)
def b(): total = 5 # (ii) -- want to change the module total
def c(): mylist.append(3) # (iii) -- mylist is a module list
def d(): total = total + 1 # (iv) -- want to change module totalRecall Solution L1.1
- (i) not needed. We only read
total. Rule R climbs the ladder: no localtotal, so it finds the global. Reading never needs a keyword. - (ii) needed →
global total. We assigntotal. By Rule A this makes a brand-new localtotal = 5and the module one is untouched. To change the module variable you must declareglobal total. - (iii) not needed.
mylist.append(3)is a mutation of the object, not a rebinding of the namemylist. There is nomylist = ..., so Rule A never fires. See Mutable vs Immutable objects. - (iv) needed →
global total. Worse than (ii): here the right-hand side readstotal, but Rule A already marked it local, so the read finds the local-with-no-value → UnboundLocalError.global totalfixes both sides.
L1.2
Name the scope level (L, E, G, or B) each reference resolves to:
len_ = len # (i) len
name = "top"
def outer():
name = "mid"
def inner():
print(name) # (ii) name
print(len) # (iii) len
inner()Recall Solution L1.2
- (i)
B(Built-in).lenisn't defined locally, in an enclosing func, or at module level → it comes from the Built-in scope. (This is the final rung of LEGB.) - (ii)
E(Enclosing).innerhas no localname; the nearest enclosing functionouterhasname = "mid", so it printsmid. It stops there — it does NOT keep climbing to the module"top". - (iii)
B(Built-in). No local, enclosing, or globallen, so again Built-in.
Level 2 — Application
(Predict the printed output.)
L2.1
x = 10
def f():
global x
x = x + 5
f()
f()
print(x)Recall Solution L2.1 — prints
20
global x cancels Rule A: x is the module variable everywhere in f. Start x = 10. First call: 10 + 5 = 15. Second call: 15 + 5 = 20. Print 20.
L2.2
def make_adder(step):
total = 0
def add():
nonlocal total
total += step
return total
return add
g = make_adder(3)
print(g(), g(), g())Recall Solution L2.2 — prints
3 6 9
add is a closure over make_adder's locals total and step. nonlocal total points add's assignment back to the enclosing total (the "next-door box"), which survives between calls. step = 3.
Calls: 0+3=3, 3+3=6, 6+3=9 → prints 3 6 9.
L2.3
count = 100
def outer():
count = 1
def inner():
global count
count = count + 1
inner()
return count
print(outer(), count)Recall Solution L2.3 — prints
1 101
Trace carefully — this is the classic global-vs-nonlocal confusion:
innersaysglobal count, so it edits the modulecount(100), NOTouter's localcount(1). Module becomes101.outerreturns its OWN localcount, which was never touched →1.- So
outer()→1, and modulecount→101. Prints1 101.
Level 3 — Analysis
(Find the bug and explain the exact mechanism.)
L3.1
This is supposed to print 1, but it crashes. What error, and why?
n = 0
def tick():
n += 1
print(n)
tick()Recall Solution L3.1 —
UnboundLocalError
n += 1 is n = n + 1, an assignment. Rule A stamps n local for all of tick. The right-hand side then reads the local n, which has no value yet → UnboundLocalError: local variable 'n' referenced before assignment. See UnboundLocalError.
Fix: add global n as the first line of tick, so n refers to the module variable on both sides.
L3.2
This raises SyntaxError. Why can't nonlocal find anything?
def f():
def g():
nonlocal k
k = 5
g()
f()Recall Solution L3.2 —
SyntaxError: no binding for nonlocal 'k' found
nonlocal needs an already-existing variable in an enclosing function scope to bind to. In f there is no k defined anywhere, so g's nonlocal k has no enclosing k to point at. Unlike global (which will happily create a module-level name), nonlocal refuses at compile time.
Fix: define k = 0 inside f before g uses it.
L3.3
Why does append work but = fails?
data = []
def add_ok():
data.append(1) # works
def add_bad():
data = data + [1] # crashesRecall Solution L3.3
add_ok:data.append(1)never writesdata = .... It only mutates the list object the name already points to. No assignment ⇒ Rule A never fires ⇒datastays the global one ⇒ works.add_bad:data = data + [1]rebinds the namedata. Rule A makesdatalocal; the right-hand side reads the not-yet-assigned local →UnboundLocalError. Fix withglobal data, or better, mutate:data.append(1). See Mutable vs Immutable objects.
Level 4 — Synthesis
(Design or repair code to meet a spec.)
L4.1
Build a bank() factory returning two functions deposit(amount) and balance(). deposit adds to a running balance; balance() returns it. No global variables allowed.
Recall Solution L4.1
def bank():
money = 0
def deposit(amount):
nonlocal money # rebinding the enclosing 'money'
money += amount
def balance():
return money # only reading -> no keyword needed
return deposit, balance
d, b = bank()
d(100); d(50)
print(b()) # 150money lives in bank. deposit rebinds it → needs nonlocal. balance only reads it → needs nothing (Rule R climbs to the enclosing scope). Both closures share the same money box, so deposits are visible to balance. Result: 150.
L4.2
Repair this so it prints enclosing_changed then global_original.
x = "global_original"
def outer():
x = "enclosing_original"
def inner():
global x # (bug is here)
x = "enclosing_changed"
inner()
print(x)
outer()
print(x)Recall Solution L4.2
As written, global x makes inner edit the module x → module becomes "enclosing_changed", but outer's local x is untouched, so it prints "enclosing_original" then "enclosing_changed" — the opposite of the spec.
Fix: change global x to nonlocal x:
def inner():
nonlocal x
x = "enclosing_changed"Now inner rebinds outer's x → outer prints "enclosing_changed"; the module x is never touched → prints "global_original". ✓
Level 5 — Mastery
(Full traces with multiple interacting scopes.)
L5.1
Predict all four printed lines.
v = "G"
def outer():
v = "E"
def mid():
nonlocal v
v = "E2"
def inner():
global v
v = "G2"
inner()
print(v) # (1)
mid()
print(v) # (2)
outer()
print(v) # (3)Recall Solution L5.1 — prints
E2, E2, G2
Track two separate boxes: the module v and outer's v.
mid:nonlocal v→ binds toouter'sv; sets it to"E2".inner:global v→ binds to the modulev; sets it to"G2". This does NOT touchouter'sv.print(v)at (1) is insidemid, whosevisouter'sv="E2".print(v)at (2) is insideouter, itsv="E2".print(v)at (3) is module level ="G2". Output:E2,E2,G2.
L5.2
This loop is meant to make three functions that return 0, 1, 2. Explain what actually happens and fix it with a default argument (a scope insight).
funcs = []
for i in range(3):
def f():
return i
funcs.append(f)
print([g() for g in funcs])Recall Solution L5.2 — actually prints
[2, 2, 2]
Each f is a closure that reads i from the enclosing scope (the module here) at call time, not creation time. By the time we call them, the loop is over and i == 2, so all three return 2.
Fix — capture the value now via a default argument:
funcs = []
for i in range(3):
def f(i=i): # default is evaluated NOW, binding a fresh local i
return i
funcs.append(f)
print([g() for g in funcs]) # [0, 1, 2]A default argument is evaluated at def time and stored per-function, so each f gets its own frozen i. Result: [0, 1, 2].
Recall Self-check: which keyword? (one-line drills)
Rebinding a module-level counter from inside a top-level function ::: global
Rebinding a variable that lives in the enclosing function of a closure ::: nonlocal
Calling .append() on a global list ::: neither (mutation, not rebinding)
print()-ing an outer variable you never assign ::: neither (pure read)
x = x + 1 where x is only defined at module top ::: global (else UnboundLocalError)
Connections
- global and nonlocal keywords
- Scope and LEGB rule
- Functions in Python
- Closures
- Mutable vs Immutable objects
- UnboundLocalError
- Pure functions and side effects