Visual walkthrough — global and nonlocal keywords
We will use three plain words the whole way. Let me define them before using them:
Reading = peeking into rooms to find a note. Assigning (name = ...) = sticking a note somewhere. The whole mystery is: which room does the note get stuck in?
Step 1 — Two ways to touch a name
WHAT. There are exactly two things you can do to a name: read it (use its value) or bind it (put name = on the left, or n += 1, for name in ..., def name, import name). These two actions follow completely different rules.
WHY start here. If you don't separate "reading" from "binding", every later rule sounds contradictory. Reading and binding are the two doors — and they open into different rooms.
PICTURE. Below, the green arrow (reading) travels outward looking for an existing note. The magenta arrow (binding) creates a note and — by default — drops it in the current room.

Step 2 — The scope ladder for READING (LEGB)
WHAT. When Python reads a name it climbs a ladder of rooms, nearest first, and stops at the first note it finds. The rungs are Local → Enclosing → Global → Built-in, i.e. LEGB.
WHY this order. Nearest room wins so that an inner function can "override" an outer name with its own. If nothing is found on any rung, you get a NameError. See Scope and LEGB rule.
PICTURE. Four nested rooms. The read-arrow enters at the innermost (Local) room and climbs until it finds the note it wants.

Step 3 — The rule that causes ALL the trouble
WHAT. For binding, Python does not climb the ladder. Instead it makes one blunt decision, once, at compile time (before running):
If a name is bound anywhere in a function body, that name is local for the entire function — every line of it.
WHY it's blunt. Python scans the whole function first, sees a count = ... somewhere, and stamps count as "belongs to this room" for the whole function — even lines above the assignment.
PICTURE. The scanner (magenta magnifying glass) sweeps the whole function body, spots one binding, and stamps the name LOCAL across every line — including the earlier read.

Step 4 — Watch it crash (the degenerate case)
WHAT. Now the famous crash. count exists globally, but inside bump we write count = count + 1.
count = 0
def bump():
count = count + 1 # boom
bump()WHY it crashes. Step 3 stamped count LOCAL for all of bump. So on the right side, count + 1 reads the local count — which has no note stuck to it yet. Result: UnboundLocalError (see UnboundLocalError). The global count = 0 is right there but Python refuses to look — the ladder is off.
PICTURE. The local room has an empty slot labelled count (reserved but bare). The read hits that empty slot and stops cold — it is not allowed to climb to the global count = 0.

Step 5 — global: turn the local stamp OFF (reach the ground floor)
WHAT. global count tells the compiler: "Do not stamp count local. Every count here IS the module-level one, and my assignments rebind that one."
count = 0
def bump():
global count
count = count + 1
bump(); bump()
print(count) # 2WHY it fixes it. With the local stamp removed, count on the right reads the global note (value 0, then 1), and count = on the left sticks the new note back onto the global slot. The counter survives across calls because the global room outlives the function.
PICTURE. A tunnel from the function room straight down to the Global (ground-floor) room. Both the read and the bind now travel through this tunnel; the local room stays empty.

Step 6 — nonlocal: reach exactly ONE room outward (next door)
WHAT. For nested functions, the variable you want may live in the enclosing function, not the module. nonlocal n binds to the nearest enclosing function's note — not local, not global. This is what makes a closure able to remember.
def make_counter():
n = 0
def step():
nonlocal n
n += 1
return n
return step
c = make_counter()
print(c(), c(), c()) # 1 2 3WHY not global here? n lives in make_counter (an Enclosing room), not the module ground floor. global n would hunt for a top-level n and find none. nonlocal reaches exactly the E rung.
PICTURE. Three rooms: module (ground) at bottom, make_counter in the middle, step on top. The nonlocal arrow reaches from step down to make_counter's n — and stops there, never touching the module.

Step 7 — The case where you need NEITHER (mutate ≠ rebind)
WHAT. Reading, and mutating an object, never trigger the local stamp — because neither one is a binding of the name.
data = []
def add():
data.append(5) # no global needed
add()
print(data) # [5]WHY. data.append(5) does not put data = anywhere. The name data is only read (to find the list), then the list itself is changed. Step 3's rule only fires on binding the name. Contrast: data = data + [5] would bind data and need global. See Mutable vs Immutable objects and Pure functions and side effects.
PICTURE. The name data (read-arrow, green) points to a list object living on the heap; .append pokes the object directly. No note is re-stuck, so no local stamp appears.

The one-picture summary
This single diagram compresses all seven steps: a name enters, we ask "reading or binding?", and the arrows show exactly which room it lands in — and how global / nonlocal redirect a binding.

Recall Feynman retelling — the whole walkthrough in plain words
Every function is a little room that can hold sticky-note names. When you just want to look at a name, you peek from your room outward: your room, then any room around it, then the front hall (the file), then Python's own cupboard — first note found wins (that's LEGB).
But when you place a note (name = ...), the rule flips. Python scans your whole room first, and if it sees you place that note anywhere, it decides "this note belongs to THIS room" — for the entire room, even lines before you placed it. So if you try to read it before placing it, you grab an empty slot and crash (UnboundLocalError). That's the "count IS defined above!" surprise: the outer count lives in a different room the read never visits.
global count says "stop making a local slot — use the front-hall note for both reading and placing." nonlocal n says "use the note in the room just outside mine — not the front hall." And if you're only looking, or you're changing the thing a note points to (like appending to a list) rather than re-sticking the note, you don't need to say anything at all.
Connections
- global and nonlocal keywords
- Scope and LEGB rule
- Functions in Python
- Closures
- Mutable vs Immutable objects
- UnboundLocalError
- Pure functions and side effects