Python Tutorial
Scope, Closures and Namespaces
When Python sees a name like total, which variable does it mean — one inside the function, one in an enclosing function, a module-level one, or a built-in? The answer follows the LEGB rule. Understanding scope explains the global and nonlocal keywords, closures (functions that remember variables from where they were created), and the famous UnboundLocalError.
Namespaces and the LEGB Rule
A namespace maps names to objects. Python looks names up in four scopes, in order: Local (inside the current function), Enclosing (outer functions), Global (module level) and Built-in (len, print...). locals() and globals() return the current namespaces as dicts.
global and nonlocal
Assigning to a name inside a function makes it local to that function for the whole function body. To assign to a module-level variable, declare global name; to assign to a variable of an enclosing function, declare nonlocal name. Global state makes code hard to test — prefer passing values in and returning results.
Closures
A closure is an inner function that remembers variables from its enclosing scope even after the outer function has returned. Closures create function factories (make_multiplier(3)), stateful counters without classes, and are the mechanism behind decorators.
Examples
The LEGB lookup order
x = "global"
def outer():
x = "enclosing"
def inner():
x = "local"
print("inner sees:", x)
inner()
print("outer sees:", x)
outer()
print("module sees:", x)
print("built-in len:", len("abc"))
inner sees: local
outer sees: enclosing
module sees: global
built-in len: 3
UnboundLocalError, global and nonlocal
counter = 0
def broken():
counter += 1 # assignment makes counter local -> error
def increment():
global counter
counter += 1
try:
broken()
except UnboundLocalError as e:
print("UnboundLocalError:", e)
increment(); increment()
print("counter:", counter)
def make_counter():
count = 0
def next_value():
nonlocal count
count += 1
return count
return next_value
tick = make_counter()
print(tick(), tick(), tick())
UnboundLocalError: cannot access local variable 'counter' where it is not associated with a value
counter: 2
1 2 3
Closures remember their environment
def make_discount(percent):
def apply(price):
return round(price * (1 - percent / 100), 2)
return apply
festival = make_discount(20)
student = make_discount(50)
print(festival(1000), student(1000))
print(festival.__closure__[0].cell_contents)
def running_average():
values = []
def add(v):
values.append(v)
return sum(values) / len(values)
return add
avg = running_average()
print(avg(10), avg(20), avg(60))
800.0 500.0
20
10.0 15.0 30.0
Common Mistakes
- Reading a global and then assigning to it in the same function without "global", causing UnboundLocalError.
- Using global variables for state that should be passed as arguments or stored in objects.
- Forgetting nonlocal when updating an enclosing variable from an inner function.
- Shadowing built-ins (list, max, id) in a scope and breaking later code.
Key Points to Remember
- Names resolve in LEGB order: Local, Enclosing, Global, Built-in.
- Assignment inside a function creates a local unless declared global or nonlocal.
- Closures capture enclosing variables and keep them alive.
- Closures enable function factories, stateful callables and decorators.
Practice the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.