diff --git a/Lib/test/test_symtable.py b/Lib/test/test_symtable.py index 9537de3756fd32..3e18dd5e697b7d 100644 --- a/Lib/test/test_symtable.py +++ b/Lib/test/test_symtable.py @@ -536,6 +536,58 @@ def test__symtable_refleak(self): class ComprehensionTests(unittest.TestCase): + def test_referenced_in_comprehension(self): + comps = [ + "[x + k + local for x in xs]", + "{x + k + local for x in xs}", + "{x: k + local for x in xs}", + "[[x + k + local for x in xs] for _ in xs]", + ] + for comp in comps: + with self.subTest(comp=comp): + st = symtable.symtable( + "def f(xs, k):\n" + " local = 1\n" + f" return {comp}\n", "?", "exec") + f = find_block(st, "f") + self.assertTrue(f.lookup("k").is_referenced()) + self.assertTrue(f.lookup("local").is_referenced()) + self.assertEqual(f.get_children(), []) + + def test_nonlocal_referenced_in_comprehension(self): + st = symtable.symtable( + "def outer(k):\n" + " def f(xs):\n" + " nonlocal k\n" + " return [x + k for x in xs]\n", "?", "exec") + f = find_block(find_block(st, "outer"), "f") + self.assertTrue(f.lookup("k").is_referenced()) + self.assertTrue(f.lookup("k").is_nonlocal()) + + def test_not_referenced_in_comprehension(self): + comps = [ + "[k for k in xs]", # The iteration variable shadows the parameter. + "[[k for _ in ()] for k in xs]", # Shadowing in a nested comprehension. + "[(lambda: k) for x in xs]", # The load is in a nested function. + "(x + k for x in xs)", # Generator expressions are not inlined. + ] + for comp in comps: + with self.subTest(comp=comp): + st = symtable.symtable( + "def f(xs, k):\n" + f" return {comp}\n", "?", "exec") + self.assertFalse(find_block(st, "f").lookup("k").is_referenced()) + + def test_class_local_not_referenced_in_comprehension(self): + st = symtable.symtable( + "def outer(k):\n" + " class C:\n" + " k = 1\n" + " values = [k for _ in ()]\n", "?", "exec") + cls = find_block(find_block(st, "outer"), "C") + # The comprehension refers to outer's k, not the class attribute. + self.assertFalse(cls.lookup("k").is_referenced()) + def get_identifiers_recursive(self, st, res): res.extend(st.get_identifiers()) for ch in st.get_children(): diff --git a/Misc/NEWS.d/next/Library/2026-10-22-12-00-00.gh-issue-160299.CompUse.rst b/Misc/NEWS.d/next/Library/2026-10-22-12-00-00.gh-issue-160299.CompUse.rst new file mode 100644 index 00000000000000..6bdfaa8a7ba1aa --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-22-12-00-00.gh-issue-160299.CompUse.rst @@ -0,0 +1,2 @@ +Fix :meth:`symtable.Symbol.is_referenced` for local and nonlocal symbols in +function scopes when their only uses are in inlined comprehensions. diff --git a/Python/symtable.c b/Python/symtable.c index 442214d1e34d6c..d4f621a474ed95 100644 --- a/Python/symtable.c +++ b/Python/symtable.c @@ -876,6 +876,20 @@ inline_comprehension(PySTEntryObject *ste, PySTEntryObject *comp, if (flags == -1 && PyErr_Occurred()) { return 0; } + // Propagate uses of enclosing bindings, but not class locals, + // which are not visible to comprehensions. + if (scope == FREE && ste->ste_type != ClassBlock && + (comp_flags & USE) && !(flags & USE)) { + PyObject *v_flags = PyLong_FromLong(flags | USE); + if (v_flags == NULL) { + return 0; + } + int ok = PyDict_SetItem(ste->ste_symbols, k, v_flags); + Py_DECREF(v_flags); + if (ok < 0) { + return 0; + } + } if ((flags & DEF_BOUND) && ste->ste_type != ClassBlock) { // free vars in comprehension that are locals in outer scope can // now simply be locals, unless they are free in comp children,