1.2.31 · D4Introduction to Programming (Python)

Exercises — global and nonlocal keywords

2,217 words10 min readBack to topic
Figure — 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 total
Recall Solution L1.1
  • (i) not needed. We only read total. Rule R climbs the ladder: no local total, so it finds the global. Reading never needs a keyword.
  • (ii) needed → global total. We assign total. By Rule A this makes a brand-new local total = 5 and the module one is untouched. To change the module variable you must declare global total.
  • (iii) not needed. mylist.append(3) is a mutation of the object, not a rebinding of the name mylist. There is no mylist = ..., so Rule A never fires. See Mutable vs Immutable objects.
  • (iv) needed → global total. Worse than (ii): here the right-hand side reads total, but Rule A already marked it local, so the read finds the local-with-no-value → UnboundLocalError. global total fixes 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). len isn'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). inner has no local name; the nearest enclosing function outer has name = "mid", so it prints mid. It stops there — it does NOT keep climbing to the module "top".
  • (iii) B (Built-in). No local, enclosing, or global len, 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:

  • inner says global count, so it edits the module count (100), NOT outer's local count (1). Module becomes 101.
  • outer returns its OWN local count, which was never touched → 1.
  • So outer()1, and module count101. Prints 1 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]  # crashes
Recall Solution L3.3
  • add_ok: data.append(1) never writes data = .... It only mutates the list object the name already points to. No assignment ⇒ Rule A never fires ⇒ data stays the global one ⇒ works.
  • add_bad: data = data + [1] rebinds the name data. Rule A makes data local; the right-hand side reads the not-yet-assigned local → UnboundLocalError. Fix with global 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())   # 150

money 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 xouter 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.

  1. mid: nonlocal v → binds to outer's v; sets it to "E2".
  2. inner: global v → binds to the module v; sets it to "G2". This does NOT touch outer's v.
  3. print(v) at (1) is inside mid, whose v is outer's v = "E2".
  4. print(v) at (2) is inside outer, its v = "E2".
  5. 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