Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions Lib/test/test_listcomps.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,12 @@ def test_references___class__(self):
"""
self._check_in_scopes(code, raises=NameError)

def test_references___class___nested(self):
code = """
res = [lambda: __class__ for _ in [1]]
"""
self._check_in_scopes(code, raises=NameError)

def test_references___class___defined(self):
code = """
__class__ = 2
Expand All @@ -180,18 +186,38 @@ def test_references___class___defined(self):
code, outputs={"res": [2]}, scopes=["module", "function"])
self._check_in_scopes(code, raises=NameError, scopes=["class"])

def test_references___class___defined_nested(self):
code = """
__class__ = 2
res = [(lambda: __class__)() for x in [1]]
"""
self._check_in_scopes(
code, outputs={"res": [2]}, scopes=["module", "function"])
self._check_in_scopes(code, raises=NameError, scopes=["class"])

def test_references___classdict__(self):
code = """
class i: [__classdict__ for x in y]
"""
self._check_in_scopes(code, raises=NameError)

def test_references___classdict___nested(self):
class _C:
res = [(lambda: __classdict__)() for _ in [1]]
self.assertIn("res", _C.res[0])

def test_references___conditional_annotations__(self):
code = """
class i: [__conditional_annotations__ for x in y]
"""
self._check_in_scopes(code, raises=NameError)

def test_references___conditional_annotations___nested(self):
code = """
class i: [lambda: __conditional_annotations__ for x in y]
"""
self._check_in_scopes(code, raises=NameError)

def test_references___class___enclosing(self):
code = """
__class__ = 2
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix a :exc:`SystemError` when compiling a class-scope comprehension containing
a ``lambda`` that references ``__class__``, ``__classdict__``, or
``__conditional_annotations__``. Patch by Bartosz Sławecki.
Comment on lines +2 to +3
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 would only mention __class__ here, since __classdict__ and __conditional_annotations__ are internal-only.

Copy link
Copy Markdown
Member Author

@johnslavik johnslavik Jun 2, 2026

Choose a reason for hiding this comment

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

Thanks.

I'm of two minds on this. I initially wrote this news entry to indeed include __class__ only, but then I looked up that we already have a precedent news entry that does mention both __classdict__ and __conditional_annotations__. Both aren't of likely special interest to the regular user, but they are of interest to me or others who know it and I liked the transparency. Maybe it's ok to keep it?

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.

Sure, I'm just being nitpicky. I don't necessarily agree with that precedent, FWIW. The changelog should be for users, not maintainers -- maintainers can read the commit log if they care.

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 do think we should mention all. The changelog can be fairly low-level and should mention changes that are visible to end users, even end users poking into things they probably shouldn't be poking into.

13 changes: 9 additions & 4 deletions Python/symtable.c
Original file line number Diff line number Diff line change
Expand Up @@ -834,17 +834,22 @@ inline_comprehension(PySTEntryObject *ste, PySTEntryObject *comp,
return 0;
}
// __class__, __classdict__ and __conditional_annotations__ are
// never allowed to be free through a class scope (see
// drop_class_free)
// not allowed to be free through a class scope (see
// drop_class_free) unless children scopes need it
if (scope == FREE && ste->ste_type == ClassBlock &&
(_PyUnicode_EqualToASCIIString(k, "__class__") ||
_PyUnicode_EqualToASCIIString(k, "__classdict__") ||
_PyUnicode_EqualToASCIIString(k, "__conditional_annotations__"))) {
scope = GLOBAL_IMPLICIT;
if (PySet_Discard(comp_free, k) < 0) {
int child_needs_free = is_free_in_any_child(comp, k);
if (child_needs_free < 0) {
return 0;
}

if (!child_needs_free) {
if (PySet_Discard(comp_free, k) < 0) {
return 0;
}
}
if (_PyUnicode_EqualToASCIIString(k, "__class__")) {
remove_dunder_class = 1;
}
Expand Down
Loading