What is a free variable in a Python nested function, and how does a cell keep it alive?
answer
- A name used here, bound out there
- The enclosing frame does not survive
- One level of indirection is added
- A one-slot container the compiler creates
- co_freevars pairs with __closure__
basics
~20 sA free variable is a name an inner function reads but does not bind, whose binding belongs to an enclosing function. CPython moves that variable out of the frame into a one-slot cell object, and the inner function holds the same cell, so the value outlives the enclosing call.
solid answer
~50 sA **free variable** of a function is a name it uses without binding, where the binding lives in an *enclosing function* rather than at module level. Ordinary locals sit in frame slots that vanish when the call returns, so that storage cannot work for a nested function that outlives its parent. The compiler therefore promotes the variable to a **cell** — an object of type `types.CellType` with a single attribute, `cell_contents`, holding one strong reference. The enclosing frame reads and writes through the cell, and every nested function that closes over the name is created holding *that same cell* in its `__closure__` tuple. This is why capture is by variable and not by value: the inner function reads the cell when it runs, so a later `nonlocal` write is visible to every closure sharing it.
code
python · 10 linesdef make_adder(n):
def add(x):
return x + n
return add
add5 = make_adder(5)
print(add5(3)) # 8
print(add5.__code__.co_freevars) # ('n',)
print(make_adder.__code__.co_cellvars) # ('n',)
print(add5.__closure__[0].cell_contents) # 5go deeper
Be ready to define a free variable in one sentence and say where its value lives after the enclosing function returns. Knowing the word 'cell' and that capture is by variable rather than by value is enough at this level.
You are expected to explain the mechanics: why a frame slot cannot hold it, what co_cellvars on the enclosing code object and co_freevars on the nested one record, and that __closure__ is None when there are no free variables.
Show that you reason about lifetime: the cell holds a strong reference, each call to a factory builds fresh cells, and nested functions from the same call share them. Be able to say when that shared slot is the feature and when it is the bug.
Own the design question of whether captured state belongs in a closure at all. Closures hide their state from introspection and from serialization; a small class or an explicitly passed argument is often the more auditable choice on a team codebase.
## What the compiler decides before anything runs Python classifies every name in a function body at compile time. A name bound anywhere in the body — by `=`, `def`, `class`, `import`, `for`, `with ... as`, `except ... as`, or as a parameter — is a **local** of that function. A name only read, with no binding in any enclosing function, is a **global** and is looked up in the module namespace when the code runs. The third case is the one that produces a closure: a name that the function only reads, but which *is* bound in an enclosing function. That is a **free variable**, and it is the only situation in which cells appear. Note what does *not* count. Reading `len` is not a free variable, it is a builtin lookup. Reading a module-level `CONFIG` from inside a nested function is not a free variable either — module globals live in a dict that outlives everything, so no special machinery is needed. Only enclosing-*function* scope needs help. ## Why a plain frame slot cannot work Locals normally live in numbered slots inside the function's frame object, which is why local access is fast: it is an index, not a dictionary lookup. The frame is discarded when the call returns. But the whole point of a factory function is that the inner function it returns keeps working afterwards, and it must still see the value it captured. If the inner function only pointed back at the outer frame, the captured value would die with the call. CPython solves this with one level of indirection. When the compiler sees that a local of the outer function is referenced by a nested function, it does not give that local a plain slot. It gives it a **cell**: a tiny container object with exactly one attribute, `cell_contents`, holding one strong reference to the current value. The outer frame stores into the cell and loads from it; the inner function object, when it is created, is handed a reference to the same cell. The cell is an ordinary reference-counted object, so it survives as long as anything points at it — including a returned inner function. ## Where the decision is visible Two code-object attributes record it. On the **enclosing** function's code object, `co_cellvars` lists the names that were promoted to cells because something nested uses them. On the **nested** function's code object, `co_freevars` lists the names it uses but does not bind. At runtime the nested function object carries `__closure__`, a tuple of cells that lines up positionally with `co_freevars`. A function with no free variables has `__closure__ is None` — not an empty tuple. A name can appear in `co_freevars` of a function that is itself nested two levels deep: the middle function neither binds nor necessarily uses the name, but it must pass the cell through to its own nested function, so the name is free in the middle function too. ## Capture is by variable, not by value Because the inner function reads `cell_contents` at call time, nothing is snapshotted when the `def` executes. If the enclosing function rebinds the name afterwards — which requires a `nonlocal` declaration, since a bare assignment in the inner function would make the name local instead — the write lands in the same cell and every closure holding it sees the new value. That shared-mutable-slot behaviour is what makes closures usable as small pieces of state, and it is also the mechanism behind several classic surprises. Cells can be **empty**. A cell is created when the enclosing frame starts, but it holds nothing until the name is first bound. If the nested function runs before that happens, reading the free variable raises `NameError`, because there is genuinely no value to load. ## The precise definition of a closure A closure in CPython is not a special kind of object. It is an ordinary function object whose `__closure__` is a non-empty tuple of cells. The same code object can be paired with a different tuple of cells to make a different closure — which is exactly what happens on every call to a factory function. Each call builds fresh cells, so two closures produced by two calls are independent, while two nested functions defined in the *same* call share their cells. The consequence worth carrying into production code is that the cell holds a **strong** reference. Whatever a closure captured stays reachable for as long as the function object itself is reachable — which, for a callback parked in a registry, can be a very long time.
- What happens if the nested function reads its free variable before the enclosing function ever assigned it?It raises `NameError`. The cell exists from the moment the enclosing frame starts, but it is empty until the name is first bound, and loading from an empty cell has no value to produce. This is the closure equivalent of `UnboundLocalError` for a plain local, and it usually means a nested function was called earlier than the author assumed.
- Is reading a module-level variable from a nested function also a free variable?No. Free variables come only from enclosing *function* scopes. A module-level name is a global, resolved in the module dictionary at call time, and it never appears in `co_freevars` or `__closure__`. Globals need no cell because the module namespace already outlives every call; that is also why rebinding one needs `global` rather than `nonlocal`.
- Does a list comprehension inside a function create its own closure over the function's names?Not since Python 3.12. PEP 709 inlines list, set and dict comprehensions into the enclosing function, so they no longer compile to a separate nested function object and have no `__closure__` of their own. A generator expression is still compiled as a separate function, so it does close over the names it uses.
saying these in an interview costs you the question
- Says the closure copies the value at def time
- Claims the inner function keeps the outer frame alive
- Thinks module globals become free variables too
- Believes __closure__ is an empty tuple when unused
- Says each closure gets its own copy of a shared name
- Assumes the captured object is weakly referenced