Skip to main content

Objects/frameobject.c (part 6)

Source:

cpython 3.14 @ ab2d84fe1023/Objects/frameobject.c

This annotation covers the Python-visible frame object and locals management. See objects_frameobject5_detail for _PyInterpreterFrame, frame creation, and stack unwinding.

Map

LinesSymbolRole
1-80PyFrameObject wrapperThe Python-visible frame object wrapping _PyInterpreterFrame
81-160frame.f_localsMaterialize locals dict from the fast locals array
161-260frame.f_linenoCurrent line number from the instruction offset
261-360Fast-to-slow conversionWhen frames escape (debugging, locals())
361-500frame_getlocalsImplementation of locals() builtin

Reading

frame.f_locals

// CPython: Objects/frameobject.c:380 frame_getlocals
static PyObject *
frame_getlocals(PyFrameObject *f, void *Py_UNUSED(ignored))
{
/* Convert the fast locals array to a dict.
Called when a frame object is accessed via sys._getframe() or debugger. */
if (PyFrame_FastToLocalsWithError(f) < 0) return NULL;
PyObject *locals = f->f_frame->f_locals;
if (locals == NULL) {
locals = f->f_frame->f_locals = PyDict_New();
}
return Py_NewRef(locals);
}

Normally, local variables live in the localsplus array for performance (direct index access, no hash). When f_locals is accessed, PyFrame_FastToLocals copies from the array to a dict. Modifications to f_locals do NOT affect the running frame unless PyFrame_LocalsToFast is called.

frame.f_lineno

// CPython: Objects/frameobject.c:440 frame_getlineno
static PyObject *
frame_getlineno(PyFrameObject *f, void *Py_UNUSED(ignored))
{
/* Decode the current instruction offset to a source line number */
int lineno = PyFrame_GetLineNumber(f);
return PyLong_FromLong(lineno);
}

int
PyFrame_GetLineNumber(PyFrameObject *f)
{
if (f->f_lineno != 0) return f->f_lineno; /* Explicit override (e.g., tracing) */
return _PyCode_InitAddressRange(f->f_frame->f_code,
_PyInterpreterFrame_LASTI(f->f_frame));
}

Line numbers are stored in co_linetable as a compressed mapping from byte offset to line number delta. _PyCode_InitAddressRange decodes this at access time.

Fast-to-slow conversion

// CPython: Objects/frameobject.c:520 PyFrame_FastToLocalsWithError
int
PyFrame_FastToLocalsWithError(PyFrameObject *f)
{
/* For each local variable (by index in co_varnames):
If the slot is filled, copy it to f_locals dict.
If the slot is empty (uninitialized local), skip it. */
PyCodeObject *co = f->f_frame->f_code;
PyObject *locals = f->f_frame->f_locals;
for (int i = 0; i < co->co_nlocalsplus; i++) {
PyObject *v = f->f_frame->localsplus[i];
if (v == NULL) continue;
PyObject *name = PyTuple_GET_ITEM(co->co_localsplusnames, i);
PyDict_SetItem(locals, name, v);
}
return 0;
}

locals() calls this function. The result is a snapshot; future changes to local variables won't be reflected unless locals() is called again.

gopy notes

PyFrameObject is objects.Frame in objects/frame.go. f_locals triggers vm.FrameFastToLocals which copies from frame.Locals slice to a objects.Dict. f_lineno calls objects.CodeGetLineNumber which decodes the linetable. frame_getlocals is the implementation of the locals() builtin in vm/eval_gen.go.