Skip to main content

Objects/cellobject.c

Source:

cpython 3.14 @ ab2d84fe1023/Objects/cellobject.c

Cell objects are the indirection layer that lets inner functions read and write variables that belong to an enclosing scope. Every free variable in a closure is backed by one PyCellObject. The compiler emits MAKE_CELL to allocate cells for the enclosing function and COPY_FREE_VARS to copy them into the callee's fast locals. At runtime the cell holds a single pointer, ob_ref, which may be NULL when the variable has not yet been assigned or has been deleted.

Map

LinesSymbolNotes
1-20PyCellObject structob_ref pointer, possibly NULL
21-45PyCell_Newallocates cell, optionally stores initial ref
46-68PyCell_Getreturns ob_ref, raises NameError if NULL
69-90PyCell_Setswaps ob_ref, decrefs old value
91-115cell_get slotdescriptor-style getter used by LOAD_DEREF
116-135cell_set slotdescriptor-style setter used by STORE_DEREF
136-155cell_repr<cell at 0x... empty> vs <cell at 0x... contents: ...>
156-160PyCell_Typetype object registration

Reading

PyCellObject and PyCell_New

The struct is minimal by design: one PyObject header plus one nullable pointer.

// CPython: Objects/cellobject.c:14 PyCellObject
typedef struct {
PyObject_HEAD
PyObject *ob_ref; /* Content of the cell, or NULL if undefined */
} PyCellObject;

PyCell_New allocates the object and, when called with a non-NULL argument, stores the initial value without an extra Py_INCREF because the reference is transferred directly.

// CPython: Objects/cellobject.c:27 PyCell_New
PyObject *
PyCell_New(PyObject *obj)
{
PyCellObject *op = PyObject_GC_New(PyCellObject, &PyCell_Type);
if (op == NULL)
return NULL;
op->ob_ref = Py_XNewRef(obj);
_PyObject_GC_TRACK(op);
return (PyObject *)op;
}

cell_get and the NULL check for unbound cells

LOAD_DEREF calls through the cell_get slot. The critical invariant is that a cell with ob_ref == NULL represents a variable that was never assigned (or was deleted with del). The slot raises NameError in that case rather than returning None, which is how Python surfaces the "free variable referenced before assignment" error.

// CPython: Objects/cellobject.c:96 cell_get
static PyObject *
cell_get(PyCellObject *self, void *Py_UNUSED(ignored))
{
if (self->ob_ref == NULL) {
PyErr_SetString(PyExc_NameError,
"free variable referenced before assignment"
" in enclosing scope");
return NULL;
}
return Py_NewRef(self->ob_ref);
}

cell_repr

The repr distinguishes the empty (unbound) case clearly, which is useful when inspecting __closure__ tuples in a debugger.

// CPython: Objects/cellobject.c:140 cell_repr
static PyObject *
cell_repr(PyCellObject *op)
{
if (op->ob_ref == NULL)
return PyUnicode_FromFormat("<cell at %p: empty>", op);
return PyUnicode_FromFormat("<cell at %p: %.80s object at %p>",
op,
Py_TYPE(op->ob_ref)->tp_name,
op->ob_ref);
}

How cells wire free variables to closure functions

When the compiler marks a variable as CO_FAST_CELL, the enclosing frame allocates a PyCellObject via MAKE_CELL. The frame stores a pointer to that cell in its localsplus array at the cell slot. The inner function object receives a tuple of these cell pointers as func_closure. When the inner function is called, COPY_FREE_VARS copies each cell reference from func_closure into the callee's own localsplus free-variable slots. Both frames then hold a reference to the same PyCellObject, so a write in either scope is immediately visible in the other.

gopy notes

Status: not yet ported.

Planned package path: objects/cell.go inside the objects package, mirroring the flat layout used for all other object types. The Go type will wrap ob_ref as a nullable *Object field. PyCell_New, PyCell_Get, and PyCell_Set map directly to constructor and accessor functions. The NULL check in cell_get must be preserved exactly: returning a Go nil is not sufficient — the port must surface a NameError through the standard errors.Raise path.