Skip to content

symtable: a name read only inside an inlined comprehension is not marked as referenced (3.12+) #160299

Open

Description

@mccryan

Bug report

Bug description

Since PEP 709 (3.12), symtable no longer creates a child table for a list, set or dict comprehension. The 3.12
What's New says the comprehension's locals now appear in the enclosing function's table (gh-109292).

However, a name from the enclosing scope that is read only inside the inlined comprehension is no longer marked
as referenced in the enclosing function's table, and no child table records the read either. The read becomes
invisible to symtable users. On 3.9 (and, as far as I know, up to 3.11), the read appears in the comprehension's own
listcomp table. Generator expressions, which are still separate scopes, are unaffected.

Reproduction:

"""Minimal reproduction: since PEP 709 (Python 3.12), a name read only inside an inlined list comprehension is not
marked as referenced in the enclosing function's symbol table, and no child table holds it either.
Run with several interpreters: python3.9 shows the read (in a 'listcomp' child table); 3.12 and 3.14 lose it."""
import symtable, sys

SRC = '''
def f(xs, k):
    return [x + k for x in xs]      # list comprehension: inlined since 3.12

def g(xs, k):
    return list(x + k for x in xs)  # generator expression: still its own scope
'''

def report():
    top = symtable.symtable(SRC, "<repro>", "exec")
    for fn in top.get_children():
        if fn.get_name() not in ("f", "g"):
            continue                 # skip implicit scopes such as __annotate__ (3.14)
        k = fn.lookup("k")
        children = {c.get_name(): sorted(s.get_name() for s in c.get_symbols() if s.is_referenced())
                    for c in fn.get_children()}
        print(f"{sys.version.split()[0]}  {fn.get_name()}: k.is_referenced() = {k.is_referenced()};  "
              f"child tables (referenced names) = {children}")

if __name__ == "__main__":
    report()

Output (macOS; CPython 3.9.6, 3.12.12, 3.14.7):

3.9.6  f: k.is_referenced() = False;  child tables (referenced names) = {'listcomp': ['k', 'x']}
3.9.6  g: k.is_referenced() = False;  child tables (referenced names) = {'genexpr': ['k', 'x']}
3.12.12  f: k.is_referenced() = False;  child tables (referenced names) = {}
3.12.12  g: k.is_referenced() = False;  child tables (referenced names) = {'genexpr': ['k', 'x']}
3.14.7  f: k.is_referenced() = False;  child tables (referenced names) = {}
3.14.7  g: k.is_referenced() = False;  child tables (referenced names) = {'genexpr': ['k', 'x']}

Expected: in f, k.is_referenced() should be True, since k is read in f's body now that the comprehension
is part of f's scope. Alternatively, symtable should expose the inlined comprehension's references some other way.

Impact: tools that use symtable to find unused parameters or variables, or dead code, get false "unused"
results. In a scan of 35,562 function scopes, this caused 1,885 false disagreements against the compiled bytecode.

Related: gh-109292 (the documentation of the PEP 709 change to symtable); gh-124697 (refactoring of inlined
comprehensions), which may already fix this on main. I have not tested 3.15 / main.

CPython versions tested on:

3.12, 3.14

Operating systems tested on:

macOS

Linked PRs

Activity

  1. added
    type-bugAn unexpected behavior, bug, or error
    interpreter-core(Objects, Python, Grammar, and Parser dirs)
    on Oct 22, 2026
  2. carljm commented on Oct 22, 2026

    @carljm
    Member

    Thanks for the report! This was already fixed in main (and thus 3.16+) by #156819, which restores the child tables for inlined comprehensions. That fix is not backportable, but we could potentially backport (just to 3.14 and 3.15) a narrow fix just for the missing "referenced" marker.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    interpreter-core(Objects, Python, Grammar, and Parser dirs)type-bugAn unexpected behavior, bug, or error

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions