Skip to content

39. Closures and Cells

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:

15

The 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 inner

The 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 needed

If 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 inner

Inside 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 inner

For 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 variable

39.4 Why Cells Exist

A normal local variable lives in a frame slot.

def f():
    x = 10
    return x

After 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 inner

After 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 cell

When 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 -> object

For:

def outer():
    x = 10
    def inner():
        return x
    return inner

the closure looks like:

inner function
    __closure__:
        cell for x
            contents: 10

Inspect it:

fn = outer()

print(fn.__closure__)
print(fn.__closure__[0].cell_contents)

Output shape:

(<cell at 0x...: int object at 0x...>,)
10

The 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',)
10

39.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 inner

inner does not use LOAD_FAST for x. It uses closure access.

Conceptually:

LOAD_DEREF x
RETURN_VALUE

LOAD_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_DEREF

Exact 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 inner

39.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 object

For:

def outer():
    x = 10

    def inner():
        return x

    return inner

the 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:

2

The 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 x

39.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
10

Both functions reference the same cell for x.

get.__closure__[0]       -> cell x
set_value.__closure__[0] -> same cell x

The 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 inc

Without nonlocal, assignment makes x local to inc:

def outer():
    x = 0

    def bad():
        x += 1
        return x

    return bad

Calling 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 = 10

nonlocal targets an enclosing function scope.

def outer():
    x = 0

    def inner():
        nonlocal x
        x = 10

Comparison:

DeclarationTarget
global xModule global dictionary
nonlocal xNearest enclosing function scope with x
no declaration with assignmentCurrent 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
cell

Example:

def outer():
    x = 1

    def inner():
        return x

Classification:

outer:
    x = local, promoted to cell
    inner = local

inner:
    x = free

This classification decides which bytecode instructions are emitted.

local variable    -> LOAD_FAST / STORE_FAST
global name       -> LOAD_GLOBAL / STORE_GLOBAL
closure variable  -> LOAD_DEREF / STORE_DEREF

39.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 graph

Example:

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 inner

The returned function needs a, but not b.

Conceptually:

inner keeps cell for a
inner does not keep b
outer frame can be destroyed

This 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: i

39.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 add

and:

def outer():
    xs = []

    def add(x):
        xs = xs + [x]

    return add

The 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 add

39.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
125

Each call to power creates a new cell for exp.

square closure:
    exp = 2

cube closure:
    exp = 3

The 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 wrapper

Use:

@log_calls
def add(a, b):
    return a + b

Conceptually:

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 function

39.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
2

The 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 inc

Object version:

class Counter:
    def __init__(self):
        self.n = 0

    def inc(self):
        self.n += 1
        return self.n

Comparison:

FeatureClosureObject
State storageCell variablesInstance attributes
Multiple operationsLess convenientNatural
IntrospectionLess explicitMore explicit
Small callback stateGood fitGood fit
Rich behaviorAwkwardBetter

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 function

This 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 bytearray

If 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 callback

The 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 callback

Now 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 inner

Calling:

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, delete

Corrected version:

def outer():
    x = 1

    def delete():
        nonlocal x
        del x

    def read():
        return x

    return read, delete

Use:

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 x

Closure cells are stored in __closure__.

def outer():
    x = []

    def inner():
        return x

    return inner

Inspect:

print(f.__defaults__)
print(outer().__closure__)

They solve different problems.

MechanismStores
Default argumentValue used when argument is omitted
Closure cellVariable 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:

15

The 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 state

39.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_return

The 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_FAST

Closure variable:

LOAD_DEREF

LOAD_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 inner

Here, 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_contents

Example:

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

MisunderstandingCorrect model
Closures copy valuesThey capture variables through cells
A closure keeps the whole outer frame aliveUsually it keeps only needed cells
nonlocal means globalIt targets an enclosing function scope
Mutating a captured list needs nonlocalOnly rebinding the name needs nonlocal
Lambdas have different closure rulesLambdas and def use the same closure model
Loop closures capture each iteration valueThey capture the loop variable
Defaults and closures are the sameDefaults store values; closures store cells
Closure variables are fast localsThey are accessed through cells

39.40 Reading Strategy

Start with:

def outer():
    x = 10

    def inner():
        return x

    return inner

Inspect:

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 functions

For 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 alive

39.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 contents

Closures explain nested functions, decorators, function factories, callback state, nonlocal, late binding in loops, and memory retention through captured variables.