Skip to main content

Python/symtable.c (part 10)

Source:

cpython 3.14 @ ab2d84fe1023/Python/symtable.c

This annotation covers scope classification in the second pass. See python_symtable9_detail for the first pass (name collection), analyze_block, and free variable propagation.

Map

LinesSymbolRole
1-80analyze_nameClassify a name as LOCAL, FREE, CELL, GLOBAL
81-180CELL vs FREE distinctionWhen to emit a cell vs a free-var slot
181-280analyze_block second passPropagate free vars upward
281-380Walrus operator (:=)Scope leakage into enclosing function
381-500_Py_MangleName mangling for __private attributes

Reading

analyze_name

// CPython: Python/symtable.c:780 analyze_name
static int
analyze_name(struct symtable *st, PyObject *scopes, PyObject *name,
long flags, PyObject *bound, PyObject *local,
PyObject *free, PyObject *global)
{
if (flags & DEF_GLOBAL) {
SET_SCOPE(scopes, name, GLOBAL_EXPLICIT);
} else if (flags & DEF_NONLOCAL) {
SET_SCOPE(scopes, name, FREE);
PySet_Add(free, name);
} else if (flags & DEF_BOUND) {
SET_SCOPE(scopes, name, LOCAL);
PySet_Add(local, name);
} else if (bound && PySet_Contains(bound, name)) {
SET_SCOPE(scopes, name, FREE);
PySet_Add(free, name);
} else {
SET_SCOPE(scopes, name, GLOBAL_IMPLICIT);
}
}

analyze_name maps each name in a scope to one of: LOCAL (assigned here), FREE (used here, assigned in enclosing scope), CELL (assigned here, used in a nested scope), GLOBAL_EXPLICIT (global statement), or GLOBAL_IMPLICIT (not defined anywhere locally).

CELL vs FREE

// CPython: Python/symtable.c:840 CELL promotion
/* After initial classification: any LOCAL name that appears in a
nested scope's FREE set must be promoted to CELL */
while ((name = PyIter_Next(free_iter)) != NULL) {
if (PySet_Contains(local, name)) {
/* local in this scope + free in child = CELL */
SET_SCOPE(scopes, name, CELL);
} else if (PySet_Contains(bound, name)) {
/* not local but bound in ancestor = FREE (pass up) */
PySet_Add(newfree, name);
}
}

A name is a CELL variable in the scope where it is assigned if it is also referenced as FREE in any nested scope. The cell object persists even after the function returns, keeping the captured value alive for closures.

analyze_block second pass

// CPython: Python/symtable.c:900 analyze_block (second pass)
/* After processing all children: walk back up from innermost to outermost,
collecting names that need to pass through intermediate scopes */
PyObject *child_free = PySet_New(NULL);
for each child scope:
analyze_block(child, newbound, child_free, newglobal);
/* child_free now contains names the child needs from above */
for (name in child_free):
if not in local and not in global:
PySet_Add(free, name); /* pass upward */

Free variables must "tunnel" through intermediate function scopes. If f closes over x from g, but g is nested in h which also doesn't define x, then h also needs a cell for x to pass it to g.

Walrus operator (:=)

// CPython: Python/symtable.c:960 walrus scope leakage
/* In a comprehension: walrus target leaks to the nearest enclosing
non-comprehension scope */
if (flags & DEF_COMP_ITER && SYMTABLE_ENTRY(st)->ste_comprehension) {
PySTEntryObject *outer = find_enclosing_function(st);
PyDict_SetItem(outer->ste_symbols, name, DEF_LOCAL | USE);
}

[y := f(x) for x in data] assigns y in the enclosing function, not inside the comprehension. The symbol table walks up to the nearest non-comprehension scope and marks the name there. This is why walrus in comprehensions prevents inlining.

_Py_Mangle

// CPython: Python/symtable.c:1060 _Py_Mangle
PyObject *
_Py_Mangle(PyObject *privateobj, PyObject *ident)
{
/* __name → _ClassName__name */
const char *p = PyUnicode_AsUTF8(ident);
if (p[0] != '_' || p[1] != '_') return Py_NewRef(ident);
/* strip trailing underscores */
...
return PyUnicode_FromFormat("_%s%s", class_name, p);
}

__attr inside a class body is rewritten to _ClassName__attr at compile time. This prevents accidental overriding in subclasses without using the __attr spelling.

gopy notes

analyze_name is compile/symtable.analyzeName in compile/symtable.go. CELL promotion is in analyzeCellsAndFree. Walrus scope leakage is handled in analyzeComprehension — it searches for the enclosing function entry and adds the target. _Py_Mangle is compile.Mangle.