Worked examples — Variable scope — LEGB rule (Local, Enclosing, Global, Built-in)
Before we begin, one shared mental picture. Every "scope" is just a labelled box holding names. When Python reads a name, it walks these boxes in a fixed order and grabs the first box that has the name.

- L = the box created the moment a function starts running.
- E = the box of a function that wraps the current one.
- G = the box for the whole file (the module).
- B = a permanent box Python ships with (
len,print,range,sum, …).
Keep this picture open. Every example below is just "which box wins, and why."
The scenario matrix
Variable scope has a small number of axes that fully determine what happens. Every real program is a combination of these cells.
| Axis | The cases we must cover |
|---|---|
| Operation | pure read · write with no keyword · write with global · write with nonlocal |
| Which box wins a read | found in L · found in E · found in G · found in B · found nowhere (NameError) |
| The assignment trap | read-before-write of a name that assignment made local (UnboundLocalError) |
| Shadowing | a name in L/E/G hides a built-in (B) |
| Degenerate / zero cases | function that assigns nothing · empty enclosing scope · nonlocal with no target · deleting a name |
| Mutation vs rebinding | changing a list's contents (no scope keyword needed) vs pointing the name at a new list (needs global/nonlocal) |
| Real-world twist | a counter shared across calls (closure) |
| Exam twist | loop/if blocks do not make scope; comprehensions do |
Below, each example is tagged with the cell(s) it covers. By the end, every cell above is ticked.
Example 1 — Read resolves in each box in turn
Steps
- Run
inner(). Python needs to readtagforprint. Why this step?print(tag)is a read, so the L→E→G→B walk begins. - Look in L (inner's box).
tag = "L-inner"is there → stop. Why this step? First match wins; Python never opens E or G. - Result:
"L-inner".
Now delete tag = "L-inner" from inner. The read walks past the empty L box into E and finds "E-outer". Delete that too, and it walks into G and finds "G-file".
Verify: with all three lines present →
L-inner. With L removed →E-outer. With L and E removed →G-file.

Example 2 — Falling all the way to Built-in
Steps
- Inside
count_chars, readlen. It's not in L (onlywordis), not in E (no enclosing function), not in G (file defines nolen). Why this step? We must exhaust L, E, G before B — that is the whole order. - Found in B →
lenis Python's built-in string length.len("hi")=2. Why this step? This is whylen,print,rangework without importing: they live permanently in B. - Now
print(banana). Readbanana: not in L, E, G, or B. Why this step? When all four boxes fail, there is nothing left to try. - Python raises
NameError: name 'banana' is not defined.
Verify:
len("hi")=2.bananais undefined everywhere →NameError.
Example 3 — The assignment trap (UnboundLocalError)
Steps
- At compile time (before the function ever runs), Python scans
add_tipand seestotal = …. Why this step? Any bare assignment to a name flags it Local for the entire function body — decided once, up front, not line by line. - So
totalis a local name throughoutadd_tip. The globaltotal = 100is now invisible inside. Why this step? The local-creation rule overrides the L→E→G walk for that specific name. - Execute
total + 10. To compute the right-hand side we must read localtotal— but it has no value yet (the assignment hasn't finished). Why this step? The read happens before the write completes, and there is no othertotalto fall back to. - Python raises
UnboundLocalError: local variable 'total' referenced before assignment.

Verify: calling
add_tip()raisesUnboundLocalError, never returns110.
Example 4 — Fixing it with global
Steps
global totaltells Python: "for this function,totalis the G name — do not make a local." Why this step? It cancels the compile-time local-creation rule from Example 3.- Now
total + 10reads the global100→110, and the assignment rebinds the global to110. Why this step? With the local rule gone, both read and write point at the same G box. add_tip()returns110; afterwards the module-leveltotalis also110.
Verify:
add_tip()=110, and the globaltotal=110afterward.
Example 5 — Fixing enclosing state with nonlocal
Steps
make_counter()runs, createsn = 0in its box, definesstep, returnsstep(call itc). Why this step? Even aftermake_counterfinishes, itsnbox survives becausestepstill points at it — that's a closure.- First
c():nonlocal naims the name at the nearest enclosing box (E), not L, not G. Why this step? Withoutnonlocal,n += 1would try to make a fresh localnand hitUnboundLocalErrorexactly like Example 3. n += 1reads E's0, writes back1, returns1. Second call →2. Third →3.
Verify:
c(), c(), c()yields(1, 2, 3).
Example 6 — Shadowing a built-in
Steps
- Line 1: read
sum→ not in G yet → found in B (the built-in) →sum([1,2,3])=6. Why this step? Before shadowing, B is wheresumlives. sum = 10puts a namesuminto the G box. Why this step? Now the L→E→G→B walk findssumin G before reaching B — the built-in is hidden, not deleted.sum + 5reads G'ssum=10→15.sum([1,2,3])reads G'ssum=10, then tries to call10(...). Why this step? An integer isn't callable →TypeError.
Verify: original
sum([1,2,3])=6; after shadowing,sum + 5=15;sum([1,2,3])raisesTypeError.
Example 7 — Mutation needs no keyword
Steps
add_book:shelf.append(3)is a read ofshelf(to find the list), then a method call that mutates it in place. Why this step? There is no assignmentshelf = …here, so the local-creation rule never fires. The read walks L→E→G, finds the list, and.appendedits that very object →[1, 2, 3]. See Mutable vs Immutable arguments.replace_shelf:shelf = [9, 9]is an assignment →shelfis local to that function. Why this step? It builds a brand-new list in the local box and throws it away when the function ends; the global list is untouched.- Final global
shelf=[1, 2, 3].
Verify: after both calls,
shelf=[1, 2, 3].
Example 8 — Degenerate nonlocal with no target
Steps
nonlocal mdemands a variablemin an enclosing function's box. Why this step?nonlocalonly reuses an existing E name; it cannot create one.outerhas nomanywhere in its body → there is no E target. Why this step? This is the empty-enclosing-scope degenerate case.- Python rejects this at compile time:
SyntaxError: no binding for nonlocal 'm' found.
Verify: the code is a
SyntaxError(compile-time), notNameError/UnboundLocalError.
Example 9 — Exam twist: blocks don't scope, comprehensions do
Steps
if/forblocks create no new box in Python.msglives inreport's single local box. Why this step? Unlike C or Java, Python scopes only by function, module, and comprehension — not by{}/indentation blocks.report(True): theifruns,msg = "yes"fills the local box → printsyes.report(False): theifis skipped, somsgwas never assigned → reading it →UnboundLocalError. Why this step? Same trap as Example 3: assignment marks it local, but this run never executed the write.- The comprehension
[k*k for k in range(4)]gives[0, 1, 4, 9], and its loop variablekdoes get its own box. Why this step? Comprehensions are one of the three scope-makers, sokdoes not leak into the module. print(k)at module level →NameError, becauseknever existed in G.
Verify:
report(True)→yes;report(False)→UnboundLocalError;[k*k for k in range(4)]=[0,1,4,9]; module-levelprint(k)→NameError.
Active Recall
Recall Self-test on the scenarios
- In Example 3, at what moment does Python decide
totalis local — compile time or run time? - Why does
shelf.append(3)need noglobal, butshelf = [9,9]would? - What error class does a
nonlocalwith no enclosing target raise, and when? - Why does
report(False)crash whilereport(True)is fine? - After
sum = 10, why doessum([1,2,3])giveTypeErrorand notNameError?
Connections
- Parent topic (Hinglish) — the core LEGB rule these examples exercise.
- Functions and Parameters — each call is what creates the L box.
- Closures and Nested Functions — Example 5's counter is a live closure over E.
- Namespaces and the dict model — every box here is really a dictionary.
- Mutable vs Immutable arguments — Example 7 is the mutate-vs-rebind distinction.
- NameError and UnboundLocalError — the two crashes catalogued above.
- Modules and import — populates the G box that Examples 4, 6, 7 read from.