skip to content

What does a Python function's __closure__ attribute hold, and how do you read a captured value?

level: middleimportance: should knowfreq 40%

answer

  1. One entry per captured name
  2. The names live on the code object
  3. Same order in both tuples
  4. A single attribute exposes the value
  5. The tuple is fixed, the slot is not

basics

~10 s

It holds a tuple of cell objects, one per free variable, aligned with code.co_freevars. Read a captured value with cell_contents, for example f.closure[0].cell_contents. A function that captures nothing has closure set to None.

solid answer

~40 s

`__closure__` is a tuple of `types.CellType` objects — one per free variable of the function, in the same order as `f.__code__.co_freevars`, which lets you zip the two into a name-to-value mapping. Each cell exposes `cell_contents`; reading it on a cell that was never filled raises `ValueError`. The attribute itself is read-only, so you cannot swap the tuple, and the tuple is immutable, so you cannot replace a cell — but `cell_contents` *is* assignable, and `types.CellType` is constructible, so you can rebuild a function over new cells with `types.FunctionType`. In practice this is debugging machinery: it answers “what did this registered callback actually capture?” without adding logging to the factory that created it.

code

pycon · 13 lines
pycon
>>> def make_adder(n):
...     def add(x):
...         return x + n
...     return add
...
>>> add5 = make_adder(5)
>>> add5.__code__.co_freevars
('n',)
>>> add5.__closure__[0].cell_contents
5
>>> dict(zip(add5.__code__.co_freevars,
...          (c.cell_contents for c in add5.__closure__)))
{'n': 5}

go deeper

for a junior

Know that the attribute exists and holds cells rather than plain values, and that you read one with cell_contents. Being able to print what a returned function captured is enough here.

for a middle

Explain the pairing with __code__.co_freevars, the None-when-empty case, and which parts are immutable: the attribute and the tuple are fixed, but cell_contents is assignable.

for a senior

Use it as a diagnostic. Be ready to describe reaching for it to explain a registry of callbacks that captured the wrong value, or to show what a long-lived closure is pinning in memory, and to say why you would not write through it in production.

for a principal

Have a position on introspection-driven fixes. Rewriting closure state from outside is unreviewable; the leadership answer is usually to change the factory so the captured state is explicit and testable, and to keep this technique in the debugging toolbox.

## The two tuples that describe a closure Every function object has a `__closure__` attribute. For a function with no free variables it is `None`; for a closure it is a tuple of **cell** objects, one for each name the function uses but does not bind. The names themselves are on the code object, in `__code__.co_freevars`, and the two tuples are **positionally aligned**: `co_freevars[i]` is the name of the variable whose cell is `__closure__[i]`. That alignment is the whole introspection API — zip the names against the contents and you have a dictionary of what the closure captured. Each cell is an instance of `types.CellType` with exactly one interesting attribute, `cell_contents`. Reading it yields the current value. Reading it on an **empty** cell — one whose variable has not been bound yet — raises `ValueError`, not `NameError`; the `NameError` is what you get from *calling* the function and having it load the empty cell, while the `ValueError` is what direct attribute access reports. ## What you can and cannot change Three layers of immutability sit between you and a captured value, and it pays to know which is which: * `__closure__` is a **read-only attribute** on the function object. Assigning to it raises `AttributeError`. * The tuple is a tuple, so `f.__closure__[0] = something` raises `TypeError`. * `cell_contents`, however, is **writable and deletable** — it has been since Python 3.7. `f.__closure__[0].cell_contents = 99` really does change what the closure sees on its next call, because the cell is the storage. `del f.__closure__[0].cell_contents` empties the cell again, after which calling the function raises `NameError`. If you need a function with the same body over *different* captured values, build one: `types.CellType(value)` makes a filled cell, `types.CellType()` makes an empty one, and `types.FunctionType(code, globals, name, argdefs, closure)` pairs a code object with a fresh tuple of cells. The closure tuple you pass must have exactly as many cells as the code object has free variables, or you get a `TypeError`. ## What this is actually for Most production code never touches `__closure__`. The cases where it earns its keep are diagnostic: * **Auditing a registry of callbacks.** A dispatch table full of functions built by a factory is opaque; `__closure__` tells you which value each entry was built for, without instrumenting the factory. * **Explaining a wrong result.** When a callback behaves as though it captured a different value than you expected, comparing `dict(zip(f.__code__.co_freevars, (c.cell_contents for c in f.__closure__)))` against your assumption settles the argument in one line. * **Explaining retention.** Because the cell holds a strong reference, whatever you see in `cell_contents` is alive for as long as the function object is. Printing what a long-lived closure captured is often the fastest way to discover that it is pinning a far larger object graph than intended. * **Testing a factory.** A test can assert on what a returned function captured rather than exercising its behaviour through a slow path. ## Reading it safely Two guards are worth building into any helper that does this. First, handle `__closure__ is None`, which is the normal state for the overwhelming majority of functions and is easy to forget until a `TypeError` about iterating `None` lands in the middle of a debug session. Second, wrap the `cell_contents` read in a `try`/`except ValueError`, because an empty cell is a legitimate state — you can observe one while the enclosing function is still running and has not yet bound the name. Be explicit about what you are *not* seeing. `__closure__` shows enclosing-function capture only. Module-level globals the function reads are not there; they resolve through `__globals__` at call time. Default argument values are not there either — they live in `__defaults__` and `__kwdefaults__`, which is a genuinely different capture mechanism with different timing. A function that appears to capture nothing may still depend heavily on both. Finally, treat writes through `cell_contents` as a debugging or last-resort tool, not a design. Rebinding a live closure's captured value mutates state that no reader of the source can see being assigned, and if two nested functions share the cell they all change at once. In normal code, build a new closure instead.

  • Can you change what an existing Python closure captured, and should you?
    You can: `f.__closure__[0].cell_contents = new_value` writes straight into the storage the closure reads. You cannot assign `__closure__` itself, which is read-only, nor replace an element of the tuple. Do it in a debugger or a narrow test, not in shipping code — it mutates state invisibly at the source level, and any sibling closure sharing that cell changes with it. Build a fresh closure instead.
  • Why is `__closure__` `None` rather than an empty tuple for an ordinary function?
    Because CPython allocates the closure tuple only when the code object actually has free variables; with none, there is nothing to allocate and the slot stays `None`. It matters in practice: any helper that iterates `__closure__` must special-case `None` first, or it will fail on the vast majority of functions it is pointed at.
  • Does `__closure__` show you every value the function depends on?
    No — only enclosing-function capture. Globals it reads are resolved through `__globals__` when the function runs, and default argument values live in `__defaults__` and `__kwdefaults__`, evaluated once at `def` time. A function with `__closure__ is None` can still be carrying a lot of captured state through defaults.

saying these in an interview costs you the question

  • Thinks __closure__ holds the values directly, not cells
  • Expects an empty tuple instead of None
  • Believes __closure__ can be reassigned on the function
  • Says globals the function reads appear in __closure__
  • Confuses captured cells with __defaults__ values
  • Assumes reading an empty cell returns None

context