Explain Python's LEGB scope rule with an example.
Python resolves names by searching four scopes in order: Local, Enclosing, Global, then Built-in. The first match wins. Assignment in a scope always creates or modifies a name in that scope unless global or nonlocal overrides this.
How to think about it
LEGB usually arrives as a warm-up — a way to reach the real targets, closures and the global/nonlocal keywords. The acronym itself is easy. The part worth your breath is one subtle rule: the moment a name is assigned anywhere inside a function, Python treats it as local for the entire body — even on lines above the assignment.
First the four scopes, searched innermost to outermost:
- Local — the function currently running.
- Enclosing — any outer functions, for closures, searched inside-out.
- Global — the module’s namespace.
- Built-in — Python’s own names (
len,range,print, …).
Reading a name walks L→E→G→B and stops at the first hit. Writing a name always targets Local, unless you say global or nonlocal.
A worked example
x = "global"
def outer():
x = "enclosing"
def inner():
x = "local"
print("inner sees:", x) # L wins
inner()
print("outer sees:", x) # E is innermost for outer()
outer()
print("module sees:", x) # G
print()
# nonlocal lets an inner function rebind the enclosing variable
def make_counter():
count = 0
def inc():
nonlocal count
count += 1
return count
return inc
c = make_counter()
print("counter:", c(), c(), c())
print()
# Built-ins live in the B layer — and can be shadowed (don't do this!)
print("len is built-in:", len([1, 2, 3]))
len = lambda x: "oops"
print("shadowed len:", len([1, 2, 3]))
del len # remove the shadow
print("restored len:", len([1, 2, 3]))
inner sees: local
outer sees: enclosing
module sees: global
counter: 1 2 3
len is built-in: 3
shadowed len: oops
restored len: 3
Each print resolved x from its own scope outward — three different answers for one name. The counter shows nonlocal at work, and the len block is a cautionary tale: assigning len at module level shadows the built-in everywhere below it, and only deleting the shadow brings the real one back.
The classic trap — UnboundLocalError
counter = 0
def increment():
counter += 1 # UnboundLocalError: counter referenced before assignment
Python sees counter += 1, decides counter is local for the whole function, and then the read half of += runs before any local value exists — so it raises. The fix is to declare intent: global counter for a module variable, nonlocal count for an enclosing one.