A closure in Python is a nested function that captures free variables from its enclosing scope. The Python Language Reference defines a closure as a function object that refers to variables from its enclosing scope — these captured variables are stored in the function object's __closure__ attribute as cell objects.
Functions that remember their birthplace
What you'll learn: What a closure is, how to create one, and why nonlocal is needed to reassign a captured variable. Closures are the foundation of decorators, callbacks, and factory functions — understanding them makes those patterns obvious.
How to read this tab: For each code example, trace the variable: where is it defined? Which function captures it? Can it outlive the outer function? Tracing ownership is the key skill for closures.
A closure is a function that "remembers" values from where it was created — even after the outer function has returned. Think of it as a function with a backpack: the backpack contains the variables it captured from its birthplace, and it carries them wherever it goes.
A closure is a function + its captured environment. When the inner function is returned, Python doesn't just return the code — it wraps the code together with the variables it needs from the outer scope. Those variables live in cell objects on the heap, not on the stack, so they survive after the outer function returns.
What is a closure?
A closure forms when a nested function (a function defined inside another function) references a variable from the enclosing function's scope. Python keeps that variable alive so the nested function can access it later — even after the outer function has returned and would normally have been cleaned up.
def make_greeting(greeting):
"""Outer function — defines the greeting."""
def greet(name):
"""Inner function — a closure that captures 'greeting'."""
return f"{greeting}, {name}!"
return greet # return the inner function, not its result
# make_greeting has finished — but the captured variable lives on
say_hello = make_greeting("Hello")
say_namaste = make_greeting("Namaste")
print(say_hello("Priya")) # Hello, Priya!
print(say_namaste("Arjun")) # Namaste, Arjun!
print(say_hello("Rohit")) # Hello, Rohit!
# 'greeting' is remembered by each closure independentlyThe nonlocal keyword signals to Python: "this variable lives in the enclosing scope." Without nonlocal, Python sees count += 1 and creates a local variable named count — which causes UnboundLocalError because you're reading it before assigning. nonlocal tells Python to look for count in the enclosing scope instead.
Three conditions for a closure
The Python Language Reference specifies that a closure forms when three things are true: there is a nested function; the nested function refers to a variable defined in the enclosing scope; and the enclosing function returns the nested function (or passes it somewhere else).
# A counter factory — each call creates an independent counter
def make_counter(start=0):
count = start # captured variable
def counter():
nonlocal count # needed to REASSIGN (not just read) the captured var
count += 1
return count
return counter
a = make_counter()
b = make_counter(10) # independent counter starting at 10
print(a()) # 1
print(a()) # 2
print(b()) # 11
print(a()) # 3 — a and b are independent
print(b()) # 12Reading a captured variable works without nonlocal. But if you want to reassign it (count += 1 is count = count + 1), Python treats it as a local variable and raises UnboundLocalError. Add nonlocal count to tell Python it lives in the enclosing scope.
✅ Beginner tab complete
- I can write a factory function that returns a customised inner function
- I know the three conditions for a closure: nested function, captured variable, returned function
- I know why nonlocal is required to REASSIGN (not just read) a captured variable
- I can explain the classic loop closure bug and how to fix it
Inspecting closures and real-world patterns
What you'll learn: How to inspect a closure's captured variables via __closure__ and __code__.co_freevars. Real-world closure patterns: memoisation, event handlers, partial application.
How to read this tab: Try inspecting __closure__[0].cell_contents on a function you've written. Seeing the captured value in the cell object makes the mechanism concrete.
cell_contents is the actual captured value. func.__closure__ is a tuple of cell objects — one per captured variable. cell.cell_contents gives you the current value. If the outer function modifies the variable after creating the closure, cell_contents reflects the updated value — because they share the same cell object.
The __closure__ attribute and cell objects
Python stores captured variables in cell objects — a special wrapper that allows the inner and outer functions to share the same variable. The closure function's __closure__ attribute holds a tuple of these cells. You can inspect them to see what was captured.
def make_multiplier(factor):
def multiply(x):
return x * factor
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
# Inspect the closure
print(double.__closure__) # (<cell at 0x...>,)
print(double.__closure__[0].cell_contents) # 2
print(triple.__closure__[0].cell_contents) # 3
# __code__.co_freevars names the captured variables
print(double.__code__.co_freevars) # ('factor',)Every decorator is a closure. When you write @timer, the wrapper function captures func from make_timer's scope. Every time you call the decorated function, the wrapper uses the captured func reference. This is why decorators work — they're closures with a specific structure.
Real-world uses of closures
Closures appear throughout Python — often without being named. Every decorator is built on closures. Every callback that remembers context is a closure. Every factory function that produces customised functions is a closure.
# 1. Memoisation using a closure
def memoised(func):
cache = {}
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper # wrapper closes over 'cache'
@memoised
def fib(n):
if n < 2: return n
return fib(n-1) + fib(n-2)
print(fib(35)) # Fast — results are cached in the closure's 'cache'
# 2. Event handlers — remembering which button was clicked
def make_button_handler(button_id):
def handler(event):
print(f"Button {button_id} clicked")
return handler # handler closes over button_id
handlers = [make_button_handler(i) for i in range(5)]
# 3. Partial application — fixing some arguments
def power(base, exponent):
return base ** exponent
square = lambda x: power(x, 2) # closure via lambda
cube = lambda x: power(x, 3)
# Better: use functools.partial (same concept, more explicit)
from functools import partial
square = partial(power, exponent=2)[lambda: i for i in range(5)] — all five lambdas return 4, not 0,1,2,3,4. They all close over the same i variable, which ends at 4 after the loop. Fix: [lambda i=i: i for i in range(5)] — bind the current value as a default argument, creating a snapshot instead of a reference.nonlocal reaches to the immediately enclosing function scope. global reaches to the module level. Never use global for closures — you want the enclosing scope, not the module scope.✅ Intermediate tab complete
- I can use __closure__[0].cell_contents to inspect a captured variable
- I can explain what __code__.co_freevars contains
- I can write a memoisation closure using a dict captured from the enclosing scope
Cell objects and CPython bytecode
What you'll learn: How CPython implements closures using heap-allocated cell objects. The MAKE_CELL, STORE_DEREF, and LOAD_DEREF bytecode instructions that distinguish closure variables from regular locals.
How to read this tab: Use dis.dis() on a function that creates a closure and compare with dis.dis() on the inner function. The difference in opcodes reveals the mechanism.
How CPython implements cell objects
When CPython's compiler sees a variable referenced in an inner function and assigned in an outer function, it marks that variable as a cell variable in the outer function and a free variable in the inner function. At runtime, the outer function allocates a cell object on the heap (not the stack) and stores the variable's value inside it. Both the outer and inner functions access the variable through this shared cell — which is why the inner function continues to see changes to the variable even after calling nonlocal.
import dis
def outer():
x = 10
def inner():
return x
return inner
# outer's bytecode uses STORE_DEREF (not STORE_FAST) for x
# inner's bytecode uses LOAD_DEREF (not LOAD_FAST) for x
dis.dis(outer)
# MAKE_CELL x ← allocates the cell object
f = outer()
dis.dis(f)
# LOAD_DEREF x ← loads through the cell pointerThe key bytecode instructions: MAKE_CELL allocates the heap-based cell; STORE_DEREF writes to the cell; LOAD_DEREF reads from it. These are distinct from the stack-based STORE_FAST/LOAD_FAST used for regular local variables, because the cell must outlive the outer function's stack frame.
✅ Expert tab complete
- I know Python allocates cell objects on the heap (not the stack) for captured variables
- I know STORE_DEREF and LOAD_DEREF are the bytecode instructions for cell variable access
- I can use dis.dis() to see the difference between a closure variable and a regular local