Variables, Scope & the LEGB Rule Interview Questions & Answers
5 questions Updated 2026-06-18
Python interview questions on the LEGB scope rule, global vs nonlocal, UnboundLocalError, late binding in loop closures, and name shadowing.
LEGB describes the order Python searches for a name: Local (inside the
current function), Enclosing (any outer functions), Global (the module's
top level), then Built-in (names like len, print). The first match wins,
and the search stops there.
x = "global"
def outer():
x = "enclosing"
def inner():
x = "local"
print(x) # "local" — Local found first
inner()
outer()
Why it matters: nearly every "why is this variable that value?" question reduces to walking L -> E -> G -> B until a name is found.
Both let you rebind a name from an outer scope instead of creating a new local.
global targets the module-level name; nonlocal targets the nearest
enclosing function scope (and that name must already exist there).
count = 0
def inc():
global count
count += 1 # rebinds module-level count
def outer():
x = 1
def inner():
nonlocal x
x = 2 # rebinds outer's x, not a new local
inner()
return x # 2
Rule of thumb: you only need these keywords to reassign an outer name — you can
always mutate an outer mutable object (e.g. list.append) without them.
Python decides a name's scope at compile time by scanning the whole function body. If a name is assigned anywhere in a function, it is treated as local for the entire function — even on lines before the assignment. Reading it before it's bound raises UnboundLocalError.
x = 10
def f():
print(x) # UnboundLocalError: x is local because of the line below
x = 20 # this assignment makes x local everywhere in f
The fix is to declare global x (or nonlocal x) if you meant the outer name, or
simply read a different name. Rule of thumb: an assignment anywhere makes the
name local everywhere in that function.
Closures capture variables, not values — this is late binding. The inner function looks up the loop variable when it is called, by which time the loop has finished and the variable holds its final value.
funcs = [lambda: i for i in range(3)]
[f() for f in funcs] # [2, 2, 2] — all see the final i
# Fix: bind the current value via a default argument
funcs = [lambda i=i: i for i in range(3)]
[f() for f in funcs] # [0, 1, 2]
The default-argument trick captures i's value at definition time. Rule of thumb:
if loop-created closures behave strangely, you're hitting late binding — bind the
value explicitly.
A local name shadows (hides) an outer name of the same identity for the duration of the scope. Assigning to it inside a function creates a separate local that leaves the module-level name untouched.
value = "module"
def f():
value = "function" # new local — shadows the global
print(value) # "function"
f()
print(value) # "module" — unchanged
list = [1, 2] # shadows the built-in list() in this scope!
Watch out for shadowing built-ins (list, id, sum, type) — it silently
breaks later calls. Rule of thumb: keep names distinct from outer scopes and
built-ins to avoid surprising lookups.
Practice tests are coming soon
Get notified when interactive mock interviews and quizzes launch.