MAKE_CELL, LOAD_DEREF, STORE_DEREF opcodes and the PyCellObject that captures variables in enclosing scopes.
Closures let a nested function use variables from an enclosing function after that enclosing function has returned.
def make_adder(n):
def add(x):
return x + n
return add
add10 = make_adder(10)
print(add10(5))Output:
15The variable n belongs to make_adder, but add still uses it later. CPython supports this by moving captured variables into cell objects. The inner function keeps references to those cells.
39.1 Nested Functions
A nested function is a function defined inside another function.
def outer():
def inner():
return 1
return innerThe def inner statement executes when outer runs. It creates a function object and binds it to the local name inner.
Calling outer() returns that function object:
fn = outer()
print(fn())The function object for inner contains:
code object
globals dictionary
defaults
keyword defaults
annotations
closure cells, if neededIf inner does not refer to outer local variables, it does not need a closure.
39.2 Free Variables
A free variable is a variable used by a code object but defined in an enclosing scope.
def outer():
x = 10
def inner():
return x
return innerInside inner, x is a free variable.
You can inspect this:
def outer():
x = 10
def inner():
return x
return inner
fn = outer()
print(fn.__code__.co_freevars)Output:
('x',)The inner code object records that it needs x from outside.
39.3 Cell Variables
A cell variable is a local variable that must be captured by an inner function.
def outer():
x = 10
def inner():
return x
return innerFor outer, x is a cell variable because an inner function uses it.
Inspect:
print(outer.__code__.co_cellvars)Output:
('x',)The same variable is seen from two directions:
outer:
x is a cell variable
inner:
x is a free variable39.4 Why Cells Exist
A normal local variable lives in a frame slot.
def f():
x = 10
return xAfter f returns, its frame can be destroyed. Its local slots disappear.
But this cannot work for closures:
def outer():
x = 10
def inner():
return x
return innerAfter outer returns, inner still needs x.
CPython solves this by storing x in a cell object.
Conceptually:
outer frame
x slot points to cell
cell contains 10
inner function
closure tuple points to same cellWhen outer returns, the frame can disappear, but the cell remains alive because inner references it.
39.5 Cell Objects
A cell object is a small container that holds a reference to another Python object.
Conceptually:
cell
contents -> objectFor:
def outer():
x = 10
def inner():
return x
return innerthe closure looks like:
inner function
__closure__:
cell for x
contents: 10Inspect it:
fn = outer()
print(fn.__closure__)
print(fn.__closure__[0].cell_contents)Output shape:
(<cell at 0x...: int object at 0x...>,)
10The cell, not the whole outer frame, is what survives.
39.6 Function Closure Tuple
A function object has a __closure__ attribute.
def outer():
x = 10
def inner():
return x
return inner
fn = outer()
print(fn.__closure__)__closure__ is either None or a tuple of cell objects.
For this example:
fn.__closure__ -> (cell_for_x,)The order of cells corresponds to fn.__code__.co_freevars.
print(fn.__code__.co_freevars)
for cell in fn.__closure__:
print(cell.cell_contents)Output:
('x',)
1039.7 Reading Closure Metadata
Use this example:
def outer(a):
b = 2
def inner(c):
return a + b + c
return inner
fn = outer(10)
print(outer.__code__.co_cellvars)
print(fn.__code__.co_freevars)
print(fn.__closure__)Output shape:
('a', 'b')
('a', 'b')
(<cell ...>, <cell ...>)The outer function has cell variables a and b.
The inner function has free variables a and b.
The returned function carries cells for both.
39.8 LOAD_DEREF
Closure variables are accessed with dereference bytecode.
For:
def outer():
x = 10
def inner():
return x
return innerinner does not use LOAD_FAST for x. It uses closure access.
Conceptually:
LOAD_DEREF x
RETURN_VALUELOAD_DEREF reads the contents of a cell.
Similarly, storing into a closure variable uses a dereference-oriented store instruction.
39.9 Disassembling a Closure
Use dis:
import dis
def outer():
x = 10
def inner():
return x
return inner
dis.dis(outer)
fn = outer()
dis.dis(fn)Look for:
MAKE_CELL
LOAD_CLOSURE
MAKE_FUNCTION
LOAD_DEREF
STORE_DEREFExact instructions vary by CPython version, but the conceptual operations are stable:
create cell for captured local
create inner function with closure
load from closure cell inside inner39.10 Closure Creation
When CPython creates a function object for a nested function, it attaches closure cells if the nested function needs free variables.
Conceptually:
outer executes
create cell for x
load inner code object
load closure cell for x
make function object with closure
return function objectFor:
def outer():
x = 10
def inner():
return x
return innerthe returned function has:
inner.__code__
inner.__globals__
inner.__closure__ = (cell_for_x,)That closure tuple is how inner sees x.
39.11 Closures Capture Variables, Not Values
Closures capture variables through cells, not snapshots of values.
def outer():
x = 1
def inner():
return x
x = 2
return inner
fn = outer()
print(fn())Output:
2The inner function sees the current contents of the cell. The cell was updated before outer returned.
Conceptually:
cell x initially contains 1
cell x later contains 2
inner reads cell x39.12 Shared Cells
Multiple inner functions can share the same cell.
def outer():
x = 0
def get():
return x
def set_value(v):
nonlocal x
x = v
return get, set_value
get, set_value = outer()
print(get())
set_value(10)
print(get())Output:
0
10Both functions reference the same cell for x.
get.__closure__[0] -> cell x
set_value.__closure__[0] -> same cell xThe cell provides shared mutable binding.
39.13 nonlocal
nonlocal tells the compiler that assignment should target an enclosing function variable, not create a new local.
def outer():
x = 0
def inc():
nonlocal x
x += 1
return x
return incWithout nonlocal, assignment makes x local to inc:
def outer():
x = 0
def bad():
x += 1
return x
return badCalling bad() raises UnboundLocalError because x += 1 tries to read local x before it has a value.
With nonlocal, CPython emits dereference operations against the outer cell.
39.14 global vs nonlocal
global targets the module namespace.
x = 0
def f():
global x
x = 10nonlocal targets an enclosing function scope.
def outer():
x = 0
def inner():
nonlocal x
x = 10Comparison:
| Declaration | Target |
|---|---|
global x | Module global dictionary |
nonlocal x | Nearest enclosing function scope with x |
| no declaration with assignment | Current local scope |
nonlocal cannot target a module global. It requires an enclosing function binding.
39.15 Closure and Scope Analysis
The compiler determines closure layout during symbol table analysis.
For each code block, it classifies names as:
local
global explicit
global implicit
free
cellExample:
def outer():
x = 1
def inner():
return xClassification:
outer:
x = local, promoted to cell
inner = local
inner:
x = freeThis classification decides which bytecode instructions are emitted.
local variable -> LOAD_FAST / STORE_FAST
global name -> LOAD_GLOBAL / STORE_GLOBAL
closure variable -> LOAD_DEREF / STORE_DEREF39.16 Closure Lifetime
A cell lives as long as something references it.
Common owners:
function closure tuple
active frame
generator frame
coroutine frame
another cell or object graphExample:
def outer():
x = [1, 2, 3]
def inner():
return x
return inner
fn = outer()The list remains alive:
fn
__closure__
cell
list [1, 2, 3]When fn becomes unreachable, the closure tuple and cell can be released, and the list can be released if no other references exist.
39.17 Closures Do Not Usually Keep Whole Frames Alive
A common misunderstanding is that a closure keeps the entire outer frame alive.
Usually it does not.
def outer():
a = "captured"
b = "not captured"
def inner():
return a
return innerThe returned function needs a, but not b.
Conceptually:
inner keeps cell for a
inner does not keep b
outer frame can be destroyedThis is more efficient than preserving the whole frame.
However, if a frame object itself is captured through introspection, then it can keep all locals alive.
39.18 Late Binding in Loops
Closures capture variables, not per-iteration values.
funcs = []
for i in range(3):
funcs.append(lambda: i)
print([f() for f in funcs])Output:
[2, 2, 2]All lambdas close over the same variable i. After the loop ends, i is 2.
This is late binding: the value is looked up when the function runs, not when it is created.
39.19 Capturing Current Values With Defaults
Use a default argument to capture the current value.
funcs = []
for i in range(3):
funcs.append(lambda i=i: i)
print([f() for f in funcs])Output:
[0, 1, 2]Here, each lambda has its own default argument value.
This uses function defaults, not closure cells.
Conceptually:
lambda i=0: i
lambda i=1: i
lambda i=2: i39.20 Closures in Comprehensions
Comprehensions have their own scope, but closures inside them can still show late binding.
funcs = [lambda: x for x in range(3)]
print([f() for f in funcs])Output:
[2, 2, 2]The loop variable x belongs to the comprehension scope, and all lambdas close over that same cell.
Use defaults to capture per-iteration values:
funcs = [lambda x=x: x for x in range(3)]
print([f() for f in funcs])Output:
[0, 1, 2]39.21 Closures and Mutability
A closure can reference a mutable object.
def outer():
xs = []
def add(x):
xs.append(x)
return xs
return add
add = outer()
print(add(1))
print(add(2))Output:
[1]
[1, 2]No nonlocal is needed because the function mutates the list object. It does not rebind the name xs.
This works:
xs.append(x)This needs nonlocal:
xs = xs + [x]The second form reassigns the name.
39.22 Rebinding vs Mutating
Compare:
def outer():
xs = []
def add(x):
xs.append(x)
return addand:
def outer():
xs = []
def add(x):
xs = xs + [x]
return addThe first mutates the object referenced by xs.
The second assigns to xs, so the compiler treats xs as local to add unless declared nonlocal.
Correct rebinding:
def outer():
xs = []
def add(x):
nonlocal xs
xs = xs + [x]
return xs
return add39.23 Closures and Function Factories
Closures are often used for function factories.
def power(exp):
def apply(x):
return x ** exp
return apply
square = power(2)
cube = power(3)
print(square(5))
print(cube(5))Output:
25
125Each call to power creates a new cell for exp.
square closure:
exp = 2
cube closure:
exp = 3The code object for apply may be shared, but the closure cells differ.
39.24 Closures and Decorators
Decorators commonly use closures.
def log_calls(fn):
def wrapper(*args, **kwargs):
print("calling", fn.__name__)
return fn(*args, **kwargs)
return wrapperUse:
@log_calls
def add(a, b):
return a + bConceptually:
def add(a, b):
return a + b
add = log_calls(add)The returned wrapper closes over the original fn.
wrapper.__closure__
cell for fn -> original add function39.25 Closures and State
Closures can store private state.
def counter():
n = 0
def inc():
nonlocal n
n += 1
return n
return inc
c = counter()
print(c())
print(c())Output:
1
2The state n is not stored on an instance. It is stored in a closure cell.
This is similar to a small object with one method and private state.
39.26 Closure State vs Object State
Closure version:
def counter():
n = 0
def inc():
nonlocal n
n += 1
return n
return incObject version:
class Counter:
def __init__(self):
self.n = 0
def inc(self):
self.n += 1
return self.nComparison:
| Feature | Closure | Object |
|---|---|---|
| State storage | Cell variables | Instance attributes |
| Multiple operations | Less convenient | Natural |
| Introspection | Less explicit | More explicit |
| Small callback state | Good fit | Good fit |
| Rich behavior | Awkward | Better |
Closures are best for small captured state. Classes are better for larger protocols.
39.27 Closures and Reference Cycles
Closures can participate in cycles.
def outer():
funcs = []
def inner():
return funcs
funcs.append(inner)
return inner
fn = outer()Reference graph:
inner function
closure cell
funcs list
inner functionThis cycle can be collected by CPython’s cyclic garbage collector when unreachable.
Cycles involving finalizers or external resources need more care.
39.28 Closures and Memory Retention
Closures can retain large objects.
def make_reader():
data = bytearray(100_000_000)
def read():
return data[0]
return read
reader = make_reader()The large data object remains alive as long as reader does.
Retention chain:
reader function
closure tuple
cell
large bytearrayIf the closure no longer needs the large object, clear or avoid capturing it.
39.29 Avoiding Accidental Capture
This captures self:
class C:
def make_callback(self):
def callback():
return self.value
return callbackThe returned callback keeps the instance alive.
Sometimes this is intended. Sometimes it creates unexpected retention.
You can capture only the needed value:
class C:
def make_callback(self):
value = self.value
def callback():
return value
return callbackNow the callback keeps value, not necessarily the whole instance.
39.30 Inspecting Closure Contents
Use __closure__ carefully:
def outer():
x = {"a": 1}
def inner():
return x
return inner
fn = outer()
for name, cell in zip(fn.__code__.co_freevars, fn.__closure__):
print(name, cell.cell_contents)Output:
x {'a': 1}This is useful for learning and debugging, but production code should rarely depend on closure internals.
39.31 Empty Cells
A closure cell can be empty in some edge cases.
Example patterns involving deletion can produce empty cells:
def outer():
x = 1
def inner():
return x
del x
return innerCalling:
fn = outer()
fn()raises an error because the cell no longer has contents.
Accessing cell.cell_contents for an empty cell raises ValueError.
39.32 Closures and del
Deleting a nonlocal variable clears the cell.
def outer():
x = 1
def delete():
nonlocal x
del x
def read:
return x
return read, deleteCorrected version:
def outer():
x = 1
def delete():
nonlocal x
del x
def read():
return x
return read, deleteUse:
read, delete = outer()
print(read())
delete()
print(read())The final read() raises because the cell is empty.
39.33 Closures and Defaults Are Different
Defaults are stored on the function object.
def f(x=[]):
return xClosure cells are stored in __closure__.
def outer():
x = []
def inner():
return x
return innerInspect:
print(f.__defaults__)
print(outer().__closure__)They solve different problems.
| Mechanism | Stores |
|---|---|
| Default argument | Value used when argument is omitted |
| Closure cell | Variable from enclosing scope |
The default-argument trick for late binding works because defaults are evaluated at function creation time.
39.34 Closures and Lambdas
lambda functions follow the same closure rules as def.
def outer():
x = 10
return lambda y: x + y
fn = outer()
print(fn(5))Output:
15The lambda has a free variable x.
print(fn.__code__.co_freevars)
print(fn.__closure__[0].cell_contents)A lambda is only a compact function expression. It does not have a special closure model.
39.35 Closures and Generators
A generator can close over variables.
def outer(limit):
def gen():
for i in range(limit):
yield i
return gen
make = outer(3)
print(list(make()))The generator function closes over limit.
When make() is called, it creates a generator object. That generator object can access the closure cell.
There are two layers:
generator function
closure cell for limit
generator object
suspended frame when running
references generator function/code/closure state39.36 Closures and Coroutines
Async functions can close over variables too.
def outer(delay):
async def wait_then_return(value):
await sleep(delay)
return value
return wait_then_returnThe async function closes over delay.
Calling it creates a coroutine object. While suspended, the coroutine keeps its frame state, and the function carries closure cells.
This can retain captured objects across awaits.
39.37 Closure Performance
Accessing a closure variable is usually slower than accessing a fast local.
Fast local:
LOAD_FASTClosure variable:
LOAD_DEREFLOAD_DEREF must read through a cell.
In most code, this cost is minor. In very hot loops, local binding can matter.
Example:
def outer(value):
def inner(xs):
v = value
total = 0
for x in xs:
total += x + v
return total
return innerHere, v is a fast local inside inner, while value is read from a cell once.
39.38 Closure Debugging
When debugging closure behavior, inspect:
fn.__code__.co_freevars
fn.__closure__
cell.cell_contentsExample:
def outer():
x = 1
y = 2
def inner():
return x + y
return inner
fn = outer()
for name, cell in zip(fn.__code__.co_freevars, fn.__closure__):
print(name, cell.cell_contents)This shows which values the function can access through its closure.
39.39 Common Misunderstandings
| Misunderstanding | Correct model |
|---|---|
| Closures copy values | They capture variables through cells |
| A closure keeps the whole outer frame alive | Usually it keeps only needed cells |
nonlocal means global | It targets an enclosing function scope |
Mutating a captured list needs nonlocal | Only rebinding the name needs nonlocal |
| Lambdas have different closure rules | Lambdas and def use the same closure model |
| Loop closures capture each iteration value | They capture the loop variable |
| Defaults and closures are the same | Defaults store values; closures store cells |
| Closure variables are fast locals | They are accessed through cells |
39.40 Reading Strategy
Start with:
def outer():
x = 10
def inner():
return x
return innerInspect:
import dis
fn = outer()
print(outer.__code__.co_cellvars)
print(fn.__code__.co_freevars)
print(fn.__closure__)
print(fn.__closure__[0].cell_contents)
dis.dis(outer)
dis.dis(fn)Then test:
nonlocal rebinding
multiple inner functions sharing one variable
lambda inside loops
default-argument capture
closures holding large objects
closures inside generators
closures inside async functionsFor each case, track:
which scope binds the name
whether the name becomes a cell variable
which inner function sees it as a free variable
which function owns the closure tuple
how long the cell remains alive39.41 Chapter Summary
Closures are CPython’s mechanism for letting nested functions access variables from enclosing function scopes. When an outer local variable is used by an inner function, CPython stores that variable in a cell. The inner function carries a closure tuple containing the needed cells.
The core model is:
outer function local used by inner function
↓
local becomes a cell variable
↓
inner function records it as a free variable
↓
function object stores closure tuple
↓
cell remains alive after outer frame returns
↓
inner function reads or writes cell contentsClosures explain nested functions, decorators, function factories, callback state, nonlocal, late binding in loops, and memory retention through captured variables.