Visual walkthrough — Variable scope — LEGB rule (Local, Enclosing, Global, Built-in)
We are extending the parent LEGB note. If a word here feels unfamiliar, it was earned there — but we rebuild everything from zero anyway.
Step 0 — The vocabulary, drawn (so no symbol is unearned)
Before any search, we need three plain words, each pinned to a picture.

In the figure above: the pale-yellow drawer is the Built-in namespace (Python fills it before your program starts — len, print, range live here). The blue drawer is your file's Global namespace. The pink drawers are created fresh every time a function runs. The arrows show the only direction Python is allowed to search: inner drawers first, outer drawers last. It may never search "downward" into a function it isn't currently inside.
Why this direction and not the reverse? Because the whole point of scope (parent note: "private workspace") is that a function's private names should win over distant ones. Searching inside-out guarantees the nearest, most specific name is found first.
Step 1 — The program we will trace
Here is the single example we will watch for the rest of the page. Read it, don't run it in your head yet — we will do that together.
WHAT we have: the same name x written three times, in three different drawers.
WHY it's the perfect specimen: only the search order decides which one print(x) sees. Nothing else varies.
PICTURE: the next steps peel this apart drawer by drawer.
Step 2 — Build the drawers in the order Python builds them
Python doesn't create all namespaces at once. It builds them as code runs.

WHAT the figure shows: a timeline. First the Built-in drawer already exists (yellow). Then the file loads and x = "GLOBAL" drops a note into the Global drawer (blue). Then outer() is called, which spawns the Enclosing drawer (pink, outer) with x = "ENCLOSING". Then inner() is called, spawning the innermost drawer (pink, inner) with x = "LOCAL".
WHY order matters: a drawer only exists while its function is running. When inner finishes, its drawer is thrown away. This is why locals are private and temporary.
Step 3 — The search: print(x) fires inside inner
Now the moment of truth. Python hits print(x) and must resolve the name x. It walks the drawers L → E → G → B, stopping at the first door that opens.

WHAT the red arrow does: it starts at the innermost pink drawer (L), asks "do you have x?", the door opens (x = "LOCAL"), search stops immediately.
WHY it never reaches E, G, or B: the first-match rule. Once a drawer answers "yes," Python takes that value and quits. The Enclosing "ENCLOSING" and Global "GLOBAL" are still there — they are simply never consulted.
Reading this line left to right: Python enters L, finds x, and the slashes over E, G, B mean "never visited." Output: LOCAL.
Step 4 — Remove one drawer's note, watch the arrow travel further
Now delete the local line so inner no longer assigns x:
def inner():
print(x) # no local x anymore
WHAT the figure shows: the red arrow reaches L, finds the drawer empty for x, so it continues outward to E. Outer's drawer has x = "ENCLOSING" — door opens, search stops.
WHY this is the whole power of the rule: the program text didn't change except one deleted line, yet the answer flips from LOCAL to ENCLOSING. The search order alone decided it.
If we also removed outer's x, the arrow would sail past E to G and print "GLOBAL". Remove that too, and it reaches B — but x isn't a built-in, so the arrow falls off the last drawer and Python raises NameError. See NameError and UnboundLocalError.
Step 5 — The degenerate case that fools everyone: UnboundLocalError
This is the case the parent note flagged as "the crucial twist." We give it a full picture because it breaks people's intuition.

WHAT happens: before bump runs even one line, Python scans the whole function body at compile time and sees count = .... That single assignment stamps count as Local for the entire function — top to bottom.
WHY it crashes: on the line count = count + 1, the right side count + 1 must read count. But count is now Local, and the Local drawer has no value in it yet (the write hasn't happened). Python is forbidden from falling back to Global here — the presence of the assignment locked the name to L.
PICTURE: in the figure the Local drawer for count is drawn as an empty box with a question mark, and the read-arrow bounces off it into a red "STOP — UnboundLocalError." Crucially the arrow is not allowed to jump to the Global drawer, even though count = 0 sits right there.
Step 6 — The two escape hatches, drawn side by side
The fix is to tell Python the assignment should target an outer drawer instead of making a fresh local.

WHAT the figure shows: two functions.
- Left:
global countredirects the assignment arrow all the way to the blue Global drawer. - Right:
nonlocal nredirects the arrow to the nearest pink Enclosing drawer (outer'sn), not the global.
count = 0
def bump():
global count
count = count + 1 # rebinds the Global
bump(); print(count) # 1
def outer():
n = 0
def inner():
nonlocal n
n += 1 # rebinds outer's n
inner(); inner()
return n
print(outer()) # 2WHY each keyword exists:
global xsays "for this whole function,xmeans the module-level name — do not create a local." The empty-local trap disappears because there is no local.nonlocal xsays "xmeans the nearest enclosing function'sx" — the exact tool that lets Closures and Nested Functions remember and update state between calls.
Step 7 — The shadowing edge case (built-in eaten by global)
One more scenario the reader must never be surprised by: what if you name a variable the same as a built-in?
len = 5 # G now has 'len'
print(len("hi")) # TypeError: 'int' object is not callableWHAT happened: when Python resolves len, it searches L → E → G → B. But now G contains len = 5, and G is searched before B. So len resolves to the integer 5, and 5("hi") is nonsense → TypeError.
WHY it's not a NameError: the name was found — just in the wrong drawer. Shadowing means a nearer drawer hides a farther one. The built-in len still exists in B; it's simply unreachable while G's note sits in front of it. Delete len from G (del len) and the built-in becomes visible again.
The one-picture summary

This final board compresses the entire walkthrough: the four nested drawers, the single read-arrow travelling L→E→G→B and stopping at the first hit, and the three coloured write-arrows showing where =, global, and nonlocal deposit a value. The red "STOP" tags mark the two ways a lookup can die: fall off the end (NameError) or read an empty local (UnboundLocalError).
Recall Feynman retelling — the whole walkthrough in plain words
You want the value behind the name x. You start in your own room (Local). If your x sticky-note is there, you take it and stop — you never even open the other doors. If your room has no x, you step out into your big sibling's room (Enclosing), then the front hallway (Global), then finally the shop that's always stocked (Built-in). First x you find wins.
Now the trap: if anywhere in your room you've promised to put an x (x = ...), Python treats x as living in your room for the entire visit. So if you try to grab it before you actually placed it, your hand closes on air — that's UnboundLocalError, and Python refuses to peek in the hallway to bail you out. Saying global x cancels the promise and points you straight at the hallway; saying nonlocal x points you at your sibling's room. And if you ever leave a note called len in the hallway, everyone finds your note before the shop's — so the real len vanishes until you tear your note down.
Active Recall
Search order of the read-arrow
Why print(x) in Step 3 prints LOCAL
x is found in the Local drawer of inner, so the search stops before ever reaching E, G, or B.Cause of UnboundLocalError
What global x redirects
What nonlocal x redirects
Shadowing len gives which error
len resolves to your Global value (e.g. an int) which isn't callable; the name WAS found, just in the wrong drawer.Error when no drawer has the name
Connections
- Parent topic — LEGB rule
- Functions and Parameters — each call spawns a fresh Local drawer.
- Closures and Nested Functions — the whole reason the Enclosing drawer (E) exists.
- Namespaces and the dict model — every drawer in these figures is literally a
dict. - Mutable vs Immutable arguments — mutating vs. rebinding a name in a drawer.
- NameError and UnboundLocalError — the two "STOP" tags in the summary figure.
- Modules and import —
importdrops names into the Global (blue) drawer.