What you'll learn
- Follow the LEGB rule to know which variable a name refers to
- Understand why assigning inside a function creates a local, and what
globalandnonlocalchange - Build a closure: a function that remembers variables from where it was created
- Fix the late-binding surprise that catches everyone who builds functions in a loop
Concept
When Python sees a name, it looks in four places in order: Local (the current function), Enclosing (any function this one is defined inside), Global (the module), and Built-in (len, print, and friends). The first match wins. That is the LEGB rule, and it explains every "which x is this" question. Reading a name from an outer scope needs no special syntax; a function can use a module-level constant without ceremony.

Assignment is different from reading. Assigning to a name inside a function makes that name local to the function, for the whole function, even before the line that assigns. So a function that reads count and later writes count = count + 1 fails with UnboundLocalError: the write made count local everywhere in the body, and the read happens before the local exists. The global statement says "this name means the module-level one", and nonlocal says "this name means the one in the enclosing function". Use both sparingly; a function that changes outer state is harder to reason about than one that returns a value.
| You want to | Inside a nested function, write | Inside a top-level function, write |
|---|---|---|
| read an outer variable | use it directly | use it directly |
| rebind an enclosing function's variable | nonlocal name |
(no enclosing function) |
| rebind a module-level variable | global name |
global name |
| mutate an outer list or dict in place | call .append, [k] = v |
same; mutation is not rebinding |
A closure is a function defined inside another function that uses the outer function's variables, and keeps using them after the outer function has returned. The inner function carries its enclosing scope with it. make_multiplier(3) returns a function that multiplies by 3, and the 3 lives on inside the returned function. Closures are how decorators work, how callbacks remember context, and how you make small configurable functions without a class.

The late-binding surprise: closures capture variables, not values. If you create several functions in a loop that all use the loop variable, they all see its final value, because the variable is looked up when the function runs, after the loop has finished. The fix is to bind the current value at creation time, usually with a default argument, lambda x, n=n: x * n, or by calling a factory function that takes the value as a parameter. Once you have seen it, you recognize it instantly.
Code
LEGB in action, and the local-versus-global distinction. Reading an outer name works; assigning creates a local unless you say otherwise.
# legb.py
greeting = "hello"
def show():
print("read global:", greeting)
def shadow():
greeting = "hi"
print("local shadows:", greeting)
def rebind_global():
global greeting
greeting = "hey"
show()
shadow()
show()
rebind_global()
show()
def broken():
print(greeting)
greeting = "oops"
broken()
read global: hello
local shadows: hi
read global: hello
read global: hey
Traceback (most recent call last):
File "legb.py", line 30, in <module>
broken()
~~~~~~^^
File "legb.py", line 26, in broken
print(greeting)
^^^^^^^^
UnboundLocalError: cannot access local variable 'greeting' where it is not associated with a valueClosures: a factory returns functions that remember their own factor, and each returned function has independent state.
# closures.py
def make_multiplier(factor):
def multiply(x):
return x * factor
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5), triple(5))
print(double.__closure__[0].cell_contents, triple.__closure__[0].cell_contents)
def make_counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
a, b = make_counter(), make_counter()
print(a(), a(), a(), "|", b())
10 15
2 3
1 2 3 | 1nonlocal versus mutation. Rebinding an enclosing variable needs the keyword; mutating an enclosing list does not, because appending is not rebinding.
# nonlocal_vs_mutate.py
def tally():
total = 0
items = []
def add(x):
nonlocal total
total += x # rebinding: needs nonlocal
items.append(x) # mutating: no keyword needed
return total, items
return add
add = tally()
add(5)
print(add(7))
(12, [5, 7])The late-binding surprise and both fixes. The first list of functions all multiply by 3; the default-argument and factory versions capture each value.
# late_binding.py
surprising = [lambda x: x * n for n in range(1, 4)]
print([f(10) for f in surprising])
fixed_default = [lambda x, n=n: x * n for n in range(1, 4)]
print([f(10) for f in fixed_default])
def make(n):
return lambda x: x * n
fixed_factory = [make(n) for n in range(1, 4)]
print([f(10) for f in fixed_factory])
[30, 30, 30]
[10, 20, 30]
[10, 20, 30]Try it yourself
Create scopes.py with a module-level settings dictionary and a function that reads it, then a function that tries to rebind it and fails, then one that mutates it in place. Write a make_greeter(greeting) factory and create two greeters. Then reproduce the late-binding bug with three button handlers in a loop and fix it with a default argument, printing the results of both versions.
Common mistakes
- Assigning to a name you meant to read. Any assignment makes the name local for the whole function; the read before it raises
UnboundLocalError. - Using
globalas a shortcut. Functions that return values compose and test easily; functions that reach out and change module state do not. - Trusting a loop variable in a closure. It is read when the closure runs, not when it was made; capture it with a default argument or a factory.
What's next
- LEGB: Local, Enclosing, Global, Built-in, first match wins; assignment makes a name local unless
globalornonlocalsays otherwise. - A closure is an inner function that keeps the variables of the function that created it; factories return configured functions.
- Closures capture variables, not values; bind loop values with a default argument.
Closures are the mechanism behind the next topic. With them you can write decorators: functions that wrap other functions to add behavior.