
Closures in Python: How Inner Functions Remember Their Environment
Here's something that looks like it shouldn't work. A function creates a local variable, defines an inner function that uses it, and returns the inner function. The outer function has finished; its local variables should be gone. Yet when you call the inner function later, it still sees the variable, with the right value.
That's a closure: a function bundled with the variables it needs from the scope where it was defined. Closures are the foundation of decorators, callback factories, and many small "configure once, call many times" helpers. They're also the source of one of Python's most common surprises, the loop where every callback returns the same value.
This post explains what a closure is, how Python stores the captured variables, how nonlocal lets you update them, the late-binding trap and its fixes, practical uses, and how to decide between a closure and a small class.
A First Closure
def make_multiplier(factor: int):
def multiply(x: int) -> int:
return x * factor
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5), triple(5)) # 10 15
make_multiplier runs twice, and each run creates a fresh factor and a fresh multiply function. Each returned function remembers the factor from its own call. double and triple are built from the same code but carry different data.
Three conditions make this a closure:
- There's a nested function (
multiplyinsidemake_multiplier). - The nested function refers to a variable from the enclosing function (
factor). - The nested function is used after the enclosing function has returned.
Where Python looks up names (local, enclosing, global, built-in) is covered in Python Scope Explained: LEGB, global, and nonlocal. This post focuses on the "E" in LEGB: what happens to enclosing variables once the enclosing function is gone.
How Python Remembers: Cells
Python doesn't keep the whole outer stack frame alive. When it compiles make_multiplier, it notices that factor is used by an inner function and stores it in a cell, a tiny container object, instead of a normal local slot. The inner function gets a reference to that cell.
You can inspect this:
print(double.__code__.co_freevars)
print(double.__closure__)
print(double.__closure__[0].cell_contents, triple.__closure__[0].cell_contents)
print(make_multiplier.__code__.co_cellvars)
('factor',)
(<cell at 0x100469ba0: int object at 0x100f4a0e0>,)
2 3
('factor',)
co_freevarslists the names the inner function uses from an enclosing scope (its free variables).__closure__is a tuple of cells, one per free variable. Each function object has its own tuple, which is whydoubleandtriplesee different values.co_cellvarson the outer function lists its locals that inner functions capture.
A function that doesn't capture anything has __closure__ set to None. That includes functions that only use globals: globals are looked up in the module each time, not captured.
Closures Capture Variables, Not Values
This is the single most important fact about closures. The cell holds the variable, and the inner function reads whatever the variable holds at the moment it runs:
def outer():
x = 1
def inner():
return x
x = 2
return inner
print(outer()()) # 2, not 1
inner was defined while x was 1, but it returns 2 because x was reassigned before inner ran. Keep this in mind; it's the root of the loop trap later in this post.
Modifying Captured Variables with nonlocal
Reading a captured variable is automatic. Assigning to one is not. Here's a counter that tries:
def broken_counter():
count = 0
def increment():
count += 1
return count
return increment
broken_counter()()
UnboundLocalError: cannot access local variable 'count' where it is not associated with a value
Any assignment to a name inside a function makes that name local to the function, for the whole function body. So count += 1 refers to a local count, which hasn't been assigned yet. Python never looks at the enclosing scope.
Declare the name nonlocal to say "this refers to the variable in the enclosing function":
def make_counter():
count = 0
def increment() -> int:
nonlocal count
count += 1
return count
return increment
c1 = make_counter()
c2 = make_counter()
print(c1(), c1(), c1(), c2()) # 1 2 3 1
Each call to make_counter() creates a new cell, so c1 and c2 count independently. This is private, persistent state with no class and no global.
Mutating Is Not Assigning
You don't need nonlocal to mutate a captured object, only to rebind the name:
def make_history():
items = []
def add(item):
items.append(item) # mutation: no nonlocal needed
return items
return add
h = make_history()
h("a")
print(h("b")) # ['a', 'b']
items.append() reads the variable items and calls a method on the list it points to. No assignment to items, so no nonlocal. Writing items = items + [item] would need it.
Several Functions Sharing One Cell
Inner functions created in the same call share the same cells, so they can cooperate on shared state:
def make_account(balance: float):
def deposit(amount: float) -> float:
nonlocal balance
balance += amount
return balance
def get_balance() -> float:
return balance
return deposit, get_balance
deposit, get_balance = make_account(100)
deposit(50)
print(get_balance()) # 150
print(deposit.__closure__[0] is get_balance.__closure__[0]) # True
That's essentially an object with two methods and one private attribute, built from functions. It's a fun demonstration, but once you have several operations over shared state, a class is usually clearer (more on that below).
The Late-Binding Trap
Here's the classic bug. You create a list of callbacks in a loop and expect each one to remember its own i:
callbacks = [lambda: i for i in range(3)]
print([cb() for cb in callbacks])
[2, 2, 2]
Every lambda captured the same variable i, not its value at creation time. By the time you call them, the loop has finished and i is 2. The same thing happens with a for loop and def, with button handlers in a GUI, and with tasks scheduled in a loop. It's not a lambda problem; it's "closures capture variables" from earlier.
The fix is to give each function its own variable. There are three common ways.
1. A default argument. Defaults are evaluated when the function is defined, so each lambda stores the current value:
callbacks = [lambda i=i: i for i in range(3)]
print([cb() for cb in callbacks]) # [0, 1, 2]
It's short and idiomatic, but it adds a parameter that callers could accidentally override.
2. functools.partial. Bind the value explicitly:
from functools import partial
def show(i: int) -> int:
return i
callbacks = [partial(show, i) for i in range(3)]
print([cb() for cb in callbacks]) # [0, 1, 2]
3. A factory function. Each call creates a new scope with a new cell:
def make_cb(i: int):
return lambda: i
callbacks = [make_cb(i) for i in range(3)]
print([cb() for cb in callbacks]) # [0, 1, 2]
The factory is the most explicit version and the easiest to extend. partial is my default for simple cases because it says exactly what it does.
Practical Uses
Function Factories
Closures are a lightweight way to produce configured functions. You set the parameters once and hand out a simple callable:
def make_validator(min_len: int, required: str = ""):
def validate(value: str) -> list[str]:
errors = []
if len(value) < min_len:
errors.append(f"must be at least {min_len} characters")
if required and required not in value:
errors.append(f"must contain {required!r}")
return errors
return validate
validate_username = make_validator(3)
validate_email = make_validator(5, required="@")
print(validate_username("al"))
print(validate_email("ada@x.io"))
print(validate_email("adax"))
['must be at least 3 characters']
[]
['must be at least 5 characters', "must contain '@'"]
The code that uses validate_email just calls a function with one argument. It doesn't need to know about the configuration.
Encapsulated State
A closure can hide state that callers can't touch directly. Here's a simple sliding-window rate limiter:
import time
from collections.abc import Callable
def rate_limiter(max_calls: int, period: float) -> Callable[[], bool]:
timestamps: list[float] = []
def allow() -> bool:
now = time.monotonic()
while timestamps and now - timestamps[0] > period:
timestamps.pop(0)
if len(timestamps) < max_calls:
timestamps.append(now)
return True
return False
return allow
allow = rate_limiter(max_calls=2, period=1.0)
print([allow() for _ in range(4)]) # [True, True, False, False]
There's no way for calling code to clear timestamps by accident; the only interface is allow().
Decorators
Every decorator with a wrapper is a closure. The wrapper captures func (and, for decorators with arguments, the configuration too):
import functools
def make_tag(tag: str):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return f"<{tag}>{func(*args, **kwargs)}</{tag}>"
return wrapper
return decorator
@make_tag("h1")
def heading(text: str) -> str:
return text
print(heading("Title")) # <h1>Title</h1>
wrapper closes over both func and tag. If you understand closures, decorators are just a particular way of using them. Python Decorators Explained covers them in full.
Callbacks
Event handlers, sorted(key=...), map(), and threading targets all take a function. Closures let you pass in context without global variables:
def sort_by_distance(points: list[tuple[float, float]], origin: tuple[float, float]):
ox, oy = origin
def distance(p: tuple[float, float]) -> float:
return ((p[0] - ox) ** 2 + (p[1] - oy) ** 2) ** 0.5
return sorted(points, key=distance)
print(sort_by_distance([(5, 5), (1, 1), (3, 0)], origin=(0, 0)))
# [(1, 1), (3, 0), (5, 5)]
Closures vs Classes
Any closure can be rewritten as a class with __call__, and vice versa:
class Multiplier:
def __init__(self, factor: int) -> None:
self.factor = factor
def __call__(self, x: int) -> int:
return x * self.factor
m = Multiplier(2)
print(m(5)) # 10
How to choose:
| Prefer a closure when... | Prefer a class when... |
|---|---|
| There's one operation | There are several operations on the same state |
| The state is small and private | Callers need to read or inspect the state |
| You're writing a decorator or a callback | You need __repr__, equality, pickling, or inheritance |
| A few lines would replace a class definition | Debugging would benefit from visible attributes |
Closures are hard to inspect (you have to dig through __closure__), can't be pickled for multiprocessing, and can't easily be extended. Classes are more verbose but more discoverable. A good rule: once you're returning a tuple of functions, write a class.
Gotchas
Class Bodies Don't Create Closures
Methods and comprehensions inside a class body can't see class-level names as enclosing variables:
class Config:
prefix = "app"
keys = [prefix + "_" + k for k in ("a", "b")]
# NameError: name 'prefix' is not defined
The class body is a namespace, not an enclosing function scope, so the comprehension's iteration can't close over prefix. Inside methods, use self.prefix or type(self).prefix. In this example, compute the list outside the class or use a loop at class level.
Captured Objects Stay Alive
A closure keeps its captured objects alive for as long as the function exists:
def make_big():
data = bytearray(10_000_000)
def size() -> int:
return len(data)
return size
s = make_big() # 10 MB stays in memory as long as `s` exists
If an inner function only needs a small piece of a large object, extract that piece into its own variable before defining the inner function, so the large object can be freed.
nonlocal Needs an Enclosing Function Variable
nonlocal x only works if x is a variable in an enclosing function. It can't refer to a module-level global (that's what global is for), and it's a SyntaxError if no enclosing function defines the name.
Conclusion
A closure is a function plus the cells holding the variables it uses from enclosing functions. Python creates those cells at compile time, gives each inner function its own __closure__ tuple, and keeps the cells alive as long as the function exists. That's how double remembers factor, how a counter keeps counting, and how every decorator's wrapper remembers the function it wraps.
Two rules cover most of the surprises: closures capture variables, not values, so loop callbacks need a default argument, partial, or a factory; and assigning to a captured name needs nonlocal. Use closures for small factories, callbacks, and decorators, and switch to a class when the state grows or callers need to see it.


