Thanks to visit codestin.com
Credit goes to github.com

Skip to content

gh-124697: Represent inlined comprehensions as subscopes in the symbol table - #156819

Open
iritkatriel wants to merge 21 commits into
python:mainfrom
iritkatriel:subscope
Open

gh-124697: Represent inlined comprehensions as subscopes in the symbol table#156819
iritkatriel wants to merge 21 commits into
python:mainfrom
iritkatriel:subscope

Conversation

@iritkatriel

@iritkatriel iritkatriel commented Sep 2, 2026

Copy link
Copy Markdown
Member

Resolves #124697

Inlined comprehensions are now represented in the symbol table as block of a new type InlinedComprehensionBlock, which is a subscope of the enclosing scope (not a separate compilation unit).

This moves the complexity of compiling inlined comprehensions from codegen to the symbol table
construction.

It removes the smell of the compiler modifying the symbol table in codegen.

@read-the-docs-community

read-the-docs-community Bot commented Sep 2, 2026

Copy link
Copy Markdown

Documentation build overview

📚 cpython-previews | 🛠️ Build #34525767 | 📁 Comparing 5216591 against main (d557d64)

  🔍 Preview build  

53 files changed · ± 53 modified

± Modified

@carljm carljm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great! Thank you for working on this ❤️

Comment thread Python/symtable.c Outdated
Comment thread Python/symtable.c
Comment thread Python/codegen.c
Comment thread Python/symtable.c Outdated
Comment thread Python/symtable.c Outdated

@JelleZijlstra JelleZijlstra left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice! This also fixes #121377 and #156664, but doesn't add tests for their reproducers. Can you add those tests?

I have a PR for the latter almost ready at #156691, but I'm happy to retire it in favor of this PR. @carljm @iritkatriel what do you think about backporting either my change or others? This PR feels a bit much to backport, but we may want to backport fixes for some of the bugs. 156691 is also a somewhat invasive change though.

Codex found that this now fails:

import sys

def outer(x):
    def inner():
        return [(lambda: x, dict(**sys._getframe().f_locals))
                for x in x]
    return inner()

outer([1])

With TypeError: dict() got multiple values for keyword argument 'x'.. #156691 has a larger change to framelocalsproxy to deal with this sort of thing; you may want to incorporate its approach.

@JelleZijlstra

Copy link
Copy Markdown
Member

Another relevant bug is #156091 but that one still crashes under this PR.

@carljm

carljm commented Sep 4, 2026

Copy link
Copy Markdown
Member

I'd be cautious about backporting an invasive fix for #156664. It's clearly a rarely-encountered edge case, given that nobody other than PyPy's test suite discovered the bug since 3.12, and the risk of introducing new bugs in an invasive fix seems high (particularly without this refactor in place.) I think "better the bugs you know than the ones you don't" applies in this case.

@iritkatriel

Copy link
Copy Markdown
Member Author

Thanks.

I pushed a fix for the frame locals bug.

I have a fix for #156691 too, but it can go in a different PR because it's an orthogonal problem.

@carljm carljm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This version looks like a major improvement! Codex did find a few issues.

Comment thread Objects/frameobject.c
if (found < 0) {
goto error;
}
if (found) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if we do this, we should apply the same deduplication to items(), values(), and __len__(). With the separate comprehension-cell and enclosing-free slots, these now disagree with keys():

import sys

def outer(x):
    def inner():
        return [(lambda: x,
                 len(sys._getframe().f_locals),
                 sys._getframe().f_locals.keys(),
                 sys._getframe().f_locals.items())
                for x in x]
    return inner()[0][1:]

print(outer([1, 2]))

Main returns (1, ['x'], [('x', 1)]); this revision returns (2, ['x'], [('x', 1), ('x', [1, 2])]). values() likewise includes both values, and dict(f_locals.items()) silently selects the enclosing list instead of the iteration value returned by dict(f_locals).

Let's extend the new frame-locals test to check that all of these operations agree.

Comment thread Python/symtable.c
_PyUnicode_EqualToASCIIString(k, "__classdict__") ||
_PyUnicode_EqualToASCIIString(k, "__conditional_annotations__"))) {
scope = GLOBAL_IMPLICIT;
_PyST_IsClassClosureName(k)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this check now counts an inlined child as needing a real class closure, even though its special-name references compile as global lookups.

For example:

class Meta(type):
    def __new__(mcls, name, bases, ns):
        ns.pop('__classcell__', None)
        return type.__new__(mcls, name, bases, ns)

class C(metaclass=Meta):
    result = [[super for _ in (0,)] for _ in (0,)]

Main creates C without supplying a __classcell__. This PR adds the cell and raises RuntimeError: __class__ not set defining 'C'. A single comprehension still succeeds.

Can we traverse through inlined children to determine whether an actual nested compilation unit needs the cell?

Comment thread Python/symtable.c
}
if (inline_comp) {
if (!inline_comprehension(ste, entry, scopes, child_free, inlined_cells)) {
if (entry->ste_type == InlinedComprehensionBlock && ste->ste_type != InlinedComprehensionBlock) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we preserve the fast-local optimization when a variable is only referenced by other inlined comprehensions?

def f(n):
    return [[x for _ in range(20)] for x in range(20)]

Deferring finalization until the enclosing scope is no longer inlined lets analyze_cells() promote x before those references are handled. Main has no cells or dereference instructions here; this revision has co_cellvars == ('x',), two MAKE_CELL instructions, and STORE_DEREF/LOAD_DEREF.

Small timeit measurements on matching debug builds showed this case around 11–17% slower. I haven't measured release-build impact.

Maybe this is fine? But it seems like it should be possible to keep this refactor behavior-preserving.

Comment thread Doc/library/symtable.rst

Used for the symbol table of a class.

.. attribute:: INLINED_COMPREHENSION

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: could we add .. versionadded:: 3.16 to this member? The enclosing enum is documented as introduced in 3.13, so the new member's availability is otherwise unclear.

Comment thread Python/compile.c
static int
compiler_resolve_inlined_free(PySTEntryObject **ste, int scope, PyObject *name)
{
while (scope == FREE && (*ste)->ste_type == InlinedComprehensionBlock) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we account for the implicit receiver synthesized by load_args_for_super() here? Its scope is 0 in the comprehension's symbol table, so this scope == FREE condition skips the enclosing-scope lookup.

This now aborts compilation in a debug build:

class A:
    def f(self):
        return 42

class B(A):
    def f(self):
        return [super().f() for _ in (0,)]

print(B().f())

Main returns [42]. The implicit receiver load passes self to codegen_nameop(), but self isn't explicitly referenced in the comprehension, so it has no symbol-table entry there. The unresolved scope reaches codegen_nameop()'s assertion. Renaming the receiver to _self gets past the assertion but raises SystemError: no locals found at runtime.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor the implementation of inlined comprehensions

3 participants