Last lesson left a mystery: make_adder(5) returned a function, make_adder finished and its variables should be gone — yet add5(1) still knows n is 5. Where does that 5 LIVE? The answer explains half the 'why does my variable...' questions you'll ever have.
What you'll learn
What
Scope is the region of code where a name is visible. Python resolves every name through four layers — Local, Enclosing, Global, Built-in (LEGB) — and a closure is an inner function that keeps its enclosing layer alive after the outer function returns.
Why
Scope bugs are quiet: a function reads a global you meant to shadow, a loop variable leaks into your closure, an UnboundLocalError appears 'randomly'. Understanding the lookup order turns these from mysteries into thirty-second fixes — and makes factories and (later) decorators fully transparent.
Where it's used
Every function you write, module-level configuration constants, counter/accumulator closures, factory-built validators, and the decorators used across every Python framework.
Where this runs in production
Flask's application-factory pattern returns route functions that close over app configuration — a closure keeping settings alive per app instance.
Custom training loops define step functions that close over the model and optimizer — the closure is what lets tf.function trace and compile them.
Instrumentation helpers build recording functions that close over service and environment tags, so every call site logs consistently without repeating config.
rate = 0.08 # global
def quote(price):
rate = 0.20 # local — shadows the global
return price * (1 + rate)
print(quote(100)) # 120.0 — used the local
print(rate) # 0.08 — global untouchedTwo independent names that happen to be spelled the same. The function's assignment created a LOCAL rate; the global never changed. Shadowing is usually harmless — until you THINK you updated the global.
Looking up 'Sam' in an office: first your own team room (Local), then the department floor (Enclosing), then the building directory (Global), finally the city phone book (Built-in). You stop at the first Sam you find — and a Sam on your team makes the department's Sam unreachable by that name. Hiring a teammate named 'print' is legal... and now the city's print is shadowed. That's why naming a variable list or sum breaks things.
Python decides at compile time: if a name is ASSIGNED anywhere in a function, that name is local EVERYWHERE in the function. Reading it before the assignment line doesn't fall back to the global — it raises UnboundLocalError. This is the rule behind the most confusing beginner error: adding `count = count + 1` at the bottom of a function suddenly breaks a read of count at the top.
total = 100
def report():
print(total) # UnboundLocalError!
total = 0 # this line makes 'total' local everywhere
# report() # would crash — comment kept for safetyRemove the assignment and the print happily reads the global. The fix is never sprinkling `global` — it's usually passing total as a PARAMETER and returning the new value. Explicit inputs and outputs beat invisible side doors.
When an inner function references a name from its enclosing function, Python attaches that variable to the inner function itself. Return the inner function and the captured variables travel with it — alive after the outer function ended. That attachment is the closure. It's not a copy of the VALUE at definition time; it's a live link to the VARIABLE (which is why each factory call gives a fresh, independent set).
Reading module-level CONSTANTS (TAX_RATE = 0.08) inside functions is normal, idiomatic Python. WRITING globals from inside functions via the `global` statement is the code smell: it creates invisible coupling — callers can't see from the signature that state changes. The professional default: functions take what they need as parameters and return what they produce. `global` appears in real codebases roughly never; treat wanting it as a design alarm.
Resolving `rate` inside an inner function — Python climbs until the first match. Click each layer.
Prove a function's variables vanish when it returns.
subtotal exists only while compute runs. After the return, the local layer is discarded.
Your task
Build make_limiter(max_calls) — a factory returning a function that allows up to max_calls invocations. Each call returns 'ok (n/max)' while allowed, then 'BLOCKED' forever after. Use a closure with nonlocal for the call count, and prove two limiters are independent.
ok (1/2) ok (2/2) BLOCKED ok (1/1)
Write your solution in the editor on the right, then hit Run.
In what order does Python search scopes for a name?
x = 10 def f(): x = 99 f() print(x) What prints?
n = 5 def bump(): n = n + 1 return n bump() What happens?
What's the reviewer's strongest argument?
Write make_tagger(tag) returning a function that wraps text as '[tag] text'. Create urgent = make_tagger('URGENT') and info = make_tagger('INFO'); print urgent('disk full') then info('backup done'). Expected: [URGENT] disk full [INFO] backup done
When does a closure need the nonlocal statement?
What happened?
Explain the LEGB rule and how shadowing follows from it.
LEGB is Python's name-resolution order: Local (the current function's names, including parameters), Enclosing (any wrapping function's names), Global (module-level names), Built-in (print, len, and friends) — searched in that order, stopping at the first match. Shadowing is the direct consequence: a name defined in an inner layer makes any same-named outer name unreachable by that spelling — not modified, just hidden. Practical corollaries worth naming: a function assigning `rate` never touches a global `rate`; a module variable named `list` silently breaks list() for the whole file because Global is checked before Built-in; and two functions can both use `i` and `total` without collision, which is exactly why local isolation is a feature rather than a limitation.
What causes UnboundLocalError, and what are the idiomatic fixes?
Python decides locality at compile time: if a name is assigned ANYWHERE in a function body — even inside a branch that never executes — that name is local for the entire function. UnboundLocalError fires when a line READS the name before any assignment has run: the lookup doesn't fall back to the global, because the name is already classified local. The classic trigger is adding `x = x + 1` or a conditional assignment to a function that previously just read a global x. Idiomatic fixes, in order of preference: pass the value as a parameter and return the new value (explicit dataflow); for closures that must rebind an enclosing variable, use nonlocal; and `global` as a last resort that usually signals the state deserves a better home — a parameter, a return value, or a class attribute.
What exactly does a closure capture — values or variables — and why does the distinction matter?
Variables, not values. The inner function keeps a live link to the enclosing VARIABLE, so it sees that variable's state at CALL time, not a snapshot from definition time. Two consequences matter. First, factories work correctly: each call to make_adder(n) creates a fresh enclosing scope, so add5 and add9 hold links to different variables and stay independent. Second, the famous loop gotcha: defining several lambdas in a loop that all reference the loop variable gives functions that all see its FINAL value, because they share one variable — the standard fix is a default argument (lambda x, i=i: ...) which really does snapshot the value at definition. Mentioning that closures underlie decorators — the wrapper closes over the wrapped function — is the natural interview capstone.
Common Mistakes to Avoid
1) Expecting a function's assignment to update a global — it creates a shadowing local. 2) Reading a name before its (anywhere-in-function) assignment — UnboundLocalError, not a global fallback. 3) Shadowing built-ins: list, sum, max, type as variable names break later calls. 4) Reaching for `global` when a parameter + return states the dataflow honestly. 5) Forgetting nonlocal when a closure rebinds its captured counter. 6) Loop-created closures all sharing the final loop value — snapshot with a default argument when you mean per-iteration capture.
Ask the AI Tutor
Try these prompts in the AI Tutor panel: • 'Quiz me: for each of these six snippets, which scope does each name resolve to?' • 'Walk me through an UnboundLocalError and its three possible fixes.' • 'Show the loop-variable closure gotcha and the default-argument fix.' • 'When should closure state graduate to a class?' • 'Interview mode: ask me what closures capture and grade my answer.'
Glossary
Scope — the region of code where a name is visible. LEGB — the lookup order: Local, Enclosing, Global, Built-in. Shadowing — an inner name hiding an outer one of the same spelling. UnboundLocalError — reading a name classified local before it's assigned. Closure — an inner function carrying live links to enclosing variables after the outer function returns. nonlocal — statement aiming an assignment at an enclosing variable. global — statement aiming an assignment at a module variable (avoid). Module-level constant — a CAPS-named global meant to be read, not written. Capture — the closure's link to an enclosing variable (variable, not value).
Recommended Resources
• Docs: 'Scopes and Namespaces' in the official Python tutorial — includes the exact LEGB example this lesson builds on. • Read: the Programming FAQ entry 'Why am I getting an UnboundLocalError?' — Python's own answer to its most-asked scope question. • Practice: write make_accumulator(start) returning a function that adds each argument to a running total (you'll need nonlocal), then explain to yourself where the total lives. • Next in DSM: Functions complete! The Data Structures module opens with Tuples — the immutable sequences you've already been returning from functions, made first-class.
Recap
✓ Name lookups climb Local → Enclosing → Global → Built-in and stop at the first hit. ✓ Assignment anywhere in a function makes that name local everywhere in it — the root of UnboundLocalError. ✓ Shadowing hides outer names (including built-ins) — it never modifies them. ✓ Closures capture VARIABLES (live links), which is why factories produce independent functions. ✓ nonlocal rebinding for closure counters; `global` writes are a design alarm. ✓ Read module constants freely; pass state as parameters and return results. Next up: Tuples — the Functions module is complete. Data Structures begins with the immutable sequence you've quietly used in every multi-value return, unpacking, and enumerate() this course.
Run your code to see the output here.