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.
Yes. In Python 3 a comprehension runs in its own implicit function scope, so its loop variable does not leak into the surrounding scope. (In Python 2 list comprehensions did leak — a common gotcha when reading old code.)
[i for i in range(3)]
print(i) # NameError — i never escaped the comprehension
x = 5
[x for x in range(3)]
print(x) # 5 — outer x untouched
The comprehension can still read enclosing names. Rule of thumb: treat each comprehension like a tiny function — its variables are private to it.
A closure stores captured variables in cell objects, exposed via
__closure__ (the cells) and __code__.co_freevars (their names). This is how you
prove a function really closed over an outer variable.
def make(n):
def f():
return n
return f
g = make(42)
g.__code__.co_freevars # ('n',)
g.__closure__[0].cell_contents # 42
Rule of thumb: if __closure__ is None, the function captured nothing and is
effectively a plain function.
Default values are evaluated once, at function-definition time, in the enclosing scope — not each call, and not in the function's local scope. This is why mutable defaults are shared and why they can't reference other parameters.
y = 10
def f(a, b=y): # b's default is bound to 10 right now
return a, b
y = 99
f(1) # (1, 10) — later change to y is irrelevant
# def g(a, b=a): ... # NameError — a isn't in scope when default is evaluated
Rule of thumb: defaults are snapshots taken at def time in the outer scope, so
avoid mutable defaults and don't expect them to see sibling arguments.
globals() returns the module's namespace dict (live — editing it changes real
globals). locals() returns a dict snapshot of the current local namespace;
writing to it generally does not reliably update real locals inside a function.
x = 1
def f():
y = 2
globals()['x'] = 99 # actually changes module x
locals()['y'] = 100 # usually has NO effect on y
return y # still 2
f()
print(x) # 99
Rule of thumb: globals() is a real handle you can mutate; locals() inside a
function is read-only-in-practice — don't rely on assigning through it.
Yes — reading an enclosing name needs nothing special; LEGB finds it. You only
need nonlocal to rebind it. The keyword's sole purpose is assignment.
def outer():
msg = "hi"
def inner():
print(msg) # fine — reads enclosing msg
inner()
def outer2():
n = 0
def inc():
n += 1 # UnboundLocalError without nonlocal
inc()
Rule of thumb: read freely across scopes; reach for global/nonlocal only the
moment you need to assign.
The class body is its own scope that exists only while the class is being
defined; it is not an enclosing scope for its methods. So a method can't see a
class-level name via plain LEGB — it must qualify it with self. or ClassName..
class C:
factor = 10
def scale(self, x):
return x * factor # NameError — factor isn't enclosing
def scale_ok(self, x):
return x * self.factor # correct
Comprehensions in a class body are also affected — they can't see other class vars.
Rule of thumb: class-body names are attributes, reachable only through self/the
class, never as free variables in methods.
del name unbinds the name in the current scope — it removes the binding, not
necessarily the object. The name still counts as local (assignment/del makes it
local), so reading it afterward raises UnboundLocalError/NameError.
def f():
x = 1
del x
print(x) # UnboundLocalError — x is local but now unbound
y = [1, 2, 3]
del y[0] # this deletes an element, not the name
Rule of thumb: del on a bare name removes the binding (and the name stays local);
del on a subscript/attribute deletes that item or attribute.
You create a global that shadows the built-in for that module. Built-ins are the last place LEGB looks, so a same-named global wins everywhere in the module — often breaking later code that expected the original.
sum = 0
total = sum([1, 2, 3]) # TypeError: 'int' object is not callable
# recover the built-in if needed:
import builtins
sum = builtins.sum
Rule of thumb: never name variables list, dict, str, sum, id, type,
input, etc. — shadowing built-ins causes confusing failures far from the cause.
A free variable is a name used in a function but bound in an enclosing function scope (the E in LEGB) — it lives in a closure cell. A global is bound at module level (the G). The compiler classifies each name as local, free, or global at compile time.
g = 1 # global
def outer():
e = 2 # will be a free var for inner
def inner():
return g + e # g is global, e is free
return inner
outer().__code__.co_freevars # ('e',) — only e is free
Rule of thumb: free = captured from an enclosing function (closure); global =
module-level. nonlocal targets free variables, global targets globals.
Functions look up global names at call time, by name, in the module dict — so changing the global before the call changes what the function sees. Locals are resolved per call and per scope, so rebinding one elsewhere can't reach in.
RATE = 0.1
def price(x):
return x * RATE # reads RATE at call time
RATE = 0.2 # patch the global
price(100) # 20.0 — sees the new value
This is why monkeypatching module-level config or functions works. Rule of thumb: globals are resolved late (by name, each call); locals are fixed within their scope.
More Fundamentals interview questions
More ways to practice
The self-quiz is live. Join our channel for updates, new content & tech tips.