Skip to main content

Python/symtable.c (part 3)

Source:

cpython 3.14 @ ab2d84fe1023/Python/symtable.c

This annotation covers nested scopes and special-case rules. See python_symtable2_detail for symtable_visit_stmt, function entry, and python_symtable_detail for the symtable struct and top-level analysis.

Map

LinesSymbolRole
1-100symtable_visit_comprehensionComprehension scope entry, iterable scoping rule
101-220symtable_visit_paramsRecord function parameters as DEF_PARAM
221-360symtable_visit_expr (lambda)Lambda: implicit function scope, single-expression body
361-480symtable_enter_block (class)Class body: __class__ cell, __classdict__
481-600analyze_nameResolve each name to LOCAL / FREE / GLOBAL / CELL

Reading

symtable_visit_comprehension

// CPython: Python/symtable.c:1580 symtable_visit_comprehension
static int
symtable_visit_comprehension(struct symtable *st, comprehension_ty e)
{
/* The outermost iterable is evaluated in the enclosing scope.
The rest of the comprehension (target, ifs, inner iters) lives
in its own implicit function scope. */
VISIT(st, expr, e->iter); /* iter is in the OUTER scope */
symtable_enter_block(st, comprehension_name, FunctionBlock, ...);
VISIT(st, expr, e->target); /* target is LOCAL to the comprehension */
VISIT_SEQ(st, expr, e->ifs);
...
symtable_exit_block(st);
}

[x for x in range(10)] creates an implicit function scope. The range(10) expression is evaluated in the enclosing scope; x is local to the comprehension. This is why [x for x in a if (a := 1)] raises NameError: the walrus target a leaks to the enclosing scope but the comprehension's a is a different binding.

symtable_visit_params

// CPython: Python/symtable.c:1180 symtable_visit_params
static int
symtable_visit_params(struct symtable *st, asdl_arg_seq *args)
{
for (int i = 0; i < asdl_seq_LEN(args); i++) {
arg_ty arg = asdl_seq_GET(args, i);
if (!symtable_add_def(st, arg->arg, DEF_PARAM, LOCATION(arg)))
return 0;
}
return 1;
}

DEF_PARAM marks a name as a positional or keyword parameter. Later, analyze_name will classify it as LOCAL if no global or nonlocal statement overrides it. Defaults and annotations are visited in the enclosing scope before entering the function block.

Lambda scoping

// CPython: Python/symtable.c:1640 symtable_visit_expr (Lambda)
case Lambda_kind:
/* Lambda is a function with a single expression body.
Enter a new FunctionBlock, visit params, then visit the body expr. */
if (e->v.Lambda.args->defaults)
VISIT_SEQ(st, expr, e->v.Lambda.args->defaults);
symtable_enter_block(st, &_Py_ID(lambda), FunctionBlock, ...);
VISIT(st, arguments, e->v.Lambda.args);
VISIT(st, expr, e->v.Lambda.body);
symtable_exit_block(st);
break;

Lambda defaults are visited before entering the lambda scope, so f = lambda x=x: x captures the outer x in the default, not the parameter.

Class body

// CPython: Python/symtable.c:720 symtable_enter_block (ClassBlock)
/* When entering a ClassBlock:
- Define __class__ as a cell variable (PEP 3135)
- Define __classdict__ for the class namespace
- Methods that reference __class__ implicitly get a free variable pointing
to the cell in the class body's scope */
if (block == ClassBlock) {
if (!symtable_add_def(st, &_Py_ID(__class__), DEF_IMPLICIT | DEF_LOCAL, loc))
return 0;
}

super() with no arguments works because CPython implicitly creates a __class__ cell in every class body. Methods that reference super() get a free variable __class__ for free.

analyze_name

// CPython: Python/symtable.c:400 analyze_name
static int
analyze_name(PySTEntryObject *ste, PyObject *scopes,
PyObject *name, long flags,
PyObject *bound, PyObject *local,
PyObject *free, PyObject *global)
{
if (flags & DEF_GLOBAL) {
/* explicit global statement */
SET_SCOPE(scopes, name, GLOBAL_EXPLICIT);
return 1;
}
if (flags & DEF_NONLOCAL) {
/* nonlocal: must find binding in an enclosing function scope */
if (!bound || !PySet_Contains(bound, name)) {
PyErr_Format(PyExc_SyntaxError,
"no binding for nonlocal '%U' found", name);
return 0;
}
SET_SCOPE(scopes, name, FREE);
return 1;
}
if (flags & DEF_BOUND) {
SET_SCOPE(scopes, name, LOCAL);
/* Add to 'local' so inner scopes can see it as a free variable */
PySet_Add(local, name);
return 1;
}
/* Name is referenced but not bound here */
if (bound && PySet_Contains(bound, name)) {
SET_SCOPE(scopes, name, FREE);
PySet_Add(free, name);
} else {
SET_SCOPE(scopes, name, GLOBAL_IMPLICIT);
}
return 1;
}

analyze_name is the core of Python's scoping rules (LEGB minus Built-in, which is runtime). A name bound in an enclosing function becomes FREE in inner functions. A name referenced but not bound anywhere in the chain becomes GLOBAL_IMPLICIT.

gopy notes

symtable_visit_comprehension is compile.SymtableVisitComprehension in compile/symtable.go. Comprehensions use FunctionBlock to get their own local scope. analyze_name is compile.AnalyzeName; the result is stored in compile.SymbolFlags per name per scope entry. Class __class__ cell is created in compile.EnterClassBlock.