Fundamentals 27 min read

How Python Implements Static Lookup for Local Variables and Its Relationship to the Local Namespace

The article explains that CPython stores function local variables in a statically‑indexed array (f_localsplus), accesses them via GETLOCAL/SETLOCAL macros, and builds the locals() dictionary on demand, showing how exec, variable assignment order, and the hidden local namespace interact with this mechanism.

Satori Komeiji's Programming Classroom
Satori Komeiji's Programming Classroom
Satori Komeiji's Programming Classroom
How Python Implements Static Lookup for Local Variables and Its Relationship to the Local Namespace

Python function parameters and variables are local variables that are accessed statically. The number of locals can be obtained with func.__code__.co_nlocals, e.g.:

def foo1():
    global x
    a = 1
    b = 2
print(foo1.__code__.co_nlocals)  # 2

When a function is called, the interpreter creates a stack frame whose f_locals field is initially NULL. The C call chain is:

// Objects/call.c
_PyFunction_Vectorcall(...){
    if (func_code->co_flags & CO_OPTIMIZED) {
        return _PyEval_Vector(tstate, f, NULL, stack, nargs, kwnames);
    }
}

// Python/ceval.c
_PyEval_Vector(..., locals, ...){
    _PyInterpreterFrame *frame = _PyEvalFramePushAndInit(tstate, func, locals, args, argcount, kwnames);
    return _PyEval_EvalFrame(tstate, frame, 0);
}

// Python/ceval.c
static _PyInterpreterFrame *_PyEvalFramePushAndInit(...){
    _PyFrame_Initialize(frame, func, locals, code, 0);
}

static inline void _PyFrame_Initialize(_PyInterpreterFrame *frame, PyFunctionObject *func,
                                        PyObject *locals, PyCodeObject *code, int null_locals_from){
    frame->f_locals = locals;  // initially NULL
}

Thus the local namespace object is not created until it is needed. When locals() is called, CPython creates a dictionary (if f_locals is NULL) and copies each name from the symbol table ( co_localsplusnames) together with its value from the f_localsplus array. The copying loop looks roughly like:

for (i = 0; i < co->co_nlocalsplus; i++) {
    PyObject *value = frame->localsplus[i];
    PyObject *name = PyTuple_GET_ITEM(co->co_localsplusnames, i);
    if (value == NULL) {
        PyObject_DelItem(locals, name);  // remove uninitialized entry
    } else {
        PyObject_SetItem(locals, name, value);
    }
}

The macros used by the bytecode instructions are:

#define GETLOCAL(i)   (frame->localsplus[i])
#define SETLOCAL(i, v) do { PyObject *tmp = GETLOCAL(i); GETLOCAL(i) = (v); Py_XDECREF(tmp); } while (0)

Bytecode LOAD_FAST and STORE_FAST therefore perform static array accesses based on the compile‑time index of the variable name.

The locals() dictionary is a dynamic view: it is rebuilt each time it is called. If a variable appears in the symbol table but its value in f_localsplus is still NULL, the entry is removed from the dictionary. This explains why code such as:

def foo():
    exec("x = 1")
    print(locals()["x"])  # works

def bar():
    exec("x = 1")
    print(locals()["x"])  # KeyError because 'x' is also a compile‑time local
    x = 123

fails: the compiler has already recorded x as a local variable, so when locals() rebuilds the dictionary it sees x with a NULL value and deletes the entry added by exec.

Similarly, using a saved reference to the locals dictionary behaves the same way because the dictionary object is shared:

def foo():
    d = locals()
    exec("y = 2")
    print(d)  # {'y': 2}
    y = 123  # now 'y' is a real local, locals() will delete the entry again

The exec statement itself runs in its own compilation unit; by default it modifies the current local namespace, creating or updating entries, but those entries are not real locals unless the name already exists in the function’s symbol table.

Key take‑aways:

Local variables are determined at compile time and stored in a contiguous array ( f_localsplus).

The bytecode instructions LOAD_FAST and STORE_FAST use the macros GETLOCAL and SETLOCAL to access that array by index. locals() builds a dictionary from the symbol table and the array; uninitialized entries are removed. exec writes to the dictionary but does not create real locals unless the name is already in the symbol table.

The local namespace object is created lazily (first locals() or exec call) and is unique per function call.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Pythonbytecodeframelocalsexeclocal variablesCPython
Satori Komeiji's Programming Classroom
Written by

Satori Komeiji's Programming Classroom

Python and Rust developer; I write about any topics you're interested in. Follow me! (#^.^#)

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.