Python Closures
Learn how Python closures work: nested functions, captured variables, nonlocal, common gotchas, and real-world use cases with runnable examples.
A closure is a nested function that remembers the variables from the enclosing scope in which it was defined, even after that outer function has returned. Closures let you attach private state to a function without using a class — making them one of Python's most elegant tools for building callbacks, factories, and stateful helpers.
This page covers how closures work, the three conditions they require, common pitfalls, and practical use cases.
What Is a Closure?
When Python executes a function, it creates a local scope that disappears once the function returns. Normally, any variable defined there is gone. A closure is the exception: if an inner function references a variable from an outer function, Python keeps that variable alive in a special cell object, and the inner function carries a reference to those cells wherever it goes.
The simplest closure is a function factory — a function that builds and returns another function:
def make_multiplier(factor):
def multiply(n):
return n * factor # 'factor' is captured from the enclosing scope
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
print(double(10)) # 20Each call to make_multiplier creates a new closure with its own independent copy of factor. double and triple are completely independent even though they were created by the same function.
Three Conditions for a Closure
A function is a closure when all three of these are true:
- There is a nested function — a function defined inside another function.
- The nested function references a variable from the enclosing scope — that variable is called a free variable.
- The enclosing function returns the nested function (or passes it somewhere else).
def outer():
message = 'Hello from outer' # free variable
def inner():
print(message) # inner references it
return inner # outer returns inner
greet = outer()
greet() # Hello from outerAfter outer() returns, its local frame is gone — but message survives inside greet.__closure__.
Inspecting a Closure
Python exposes closure cells through the __closure__ attribute:
def make_adder(n):
def add(x):
return x + n
return add
add5 = make_adder(5)
print(add5(3)) # 8
print(add5.__closure__) # (<cell at 0x...>,)
print(add5.__closure__[0].cell_contents) # 5__closure__ is a tuple of cell objects — one per captured variable. If a function is not a closure, __closure__ is None.
Modifying Captured Variables with nonlocal
By default, you can read a captured variable but not rebind it. Attempting to assign to it creates a new local variable instead, which is usually not what you want. Use the nonlocal keyword to tell Python you mean the enclosing scope's variable:
def make_counter(start=0):
count = start
def increment(step=1):
nonlocal count # rebind the enclosing 'count', not a new local
count += step
return count
return increment
counter = make_counter()
print(counter()) # 1
print(counter()) # 2
print(counter(5)) # 7
counter2 = make_counter(10)
print(counter2()) # 11
print(counter()) # 8 — counter is unaffectedEach call to make_counter produces an independent count cell. counter and counter2 do not share state.
For a deeper look at how Python decides which scope a variable belongs to, see Python Scope.
Common Gotcha: Closures in Loops
A classic mistake is creating closures inside a loop and expecting each one to capture the loop variable's current value:
# Wrong — all functions capture the same 'i' cell
funcs = []
for i in range(3):
funcs.append(lambda: i)
print([f() for f in funcs]) # [2, 2, 2] — not [0, 1, 2]All three lambdas share one cell that holds the loop variable i. By the time they are called, i has reached its final value of 2.
Fix 1: Default argument (captures by value)
funcs = []
for i in range(3):
funcs.append(lambda i=i: i) # default arg is evaluated immediately
print([f() for f in funcs]) # [0, 1, 2]Fix 2: Factory function
def make_func(i):
def f():
return i
return f
funcs = [make_func(i) for i in range(3)]
print([f() for f in funcs]) # [0, 1, 2]The factory function creates a new scope — and therefore a new cell — for each iteration. This is the more explicit and readable approach.
Practical Use Cases
Partial Application
Closures are a lightweight alternative to functools.partial when you need a pre-configured version of a function:
def make_power(exponent):
def power(base):
return base ** exponent
return power
square = make_power(2)
cube = make_power(3)
print(square(4)) # 16
print(cube(3)) # 27Simple Memoization
A closure can hold a cache dictionary that persists between calls:
def make_memoized(func):
cache = {}
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
@make_memoized
def slow_square(n):
return n * n
print(slow_square(4)) # 16
print(slow_square(4)) # 16 (served from cache)
print(slow_square(7)) # 49This pattern is exactly how Python Decorators work under the hood — a decorator is just a closure that wraps another function.
Callback Configuration
Closures are handy for building callbacks that need a bit of context baked in:
def make_logger(prefix):
def log(message):
print(f'[{prefix}] {message}')
return log
info = make_logger('INFO')
error = make_logger('ERROR')
info('Server started') # [INFO] Server started
error('Disk full') # [ERROR] Disk fullClosures vs. Classes
A closure and a single-method class often solve the same problem. Choose based on complexity:
| Situation | Prefer |
|---|---|
| One piece of state, one behaviour | Closure |
| Multiple methods or public attributes | Class |
| Needs to be serialised (e.g. pickle) | Class |
| Passing a callback to another function | Closure |
# Class approach
class Counter:
def __init__(self, start=0):
self.count = start
def increment(self, step=1):
self.count += step
return self.count
# Closure approach
def make_counter(start=0):
count = start
def increment(step=1):
nonlocal count
count += step
return count
return incrementBoth produce identical behaviour. The closure is shorter; the class is more discoverable and extensible.
Conclusion
Closures let a nested function carry its own private state by remembering the variables from the scope where it was defined. The key points are:
- A closure requires a nested function, a free variable, and the inner function being returned or passed on.
- Use
nonlocalwhen you need to rebind (not just read) a captured variable. - Avoid the loop-variable gotcha: use a factory function or a default argument to snapshot the value at each iteration.
- Closures are the foundation of decorators and are closely related to Python's scope rules.