https://github.com/python/cpython/commit/9eab48adf85739a450e66b7a72a38996f9a605c7
commit: 9eab48adf85739a450e66b7a72a38996f9a605c7
branch: 3.14
author: Carl Meyer <[email protected]>
committer: carljm <[email protected]>
date: 2026-10-23T18:31:06Z
summary:

[3.14] gh-160299: Preserve references from inlined comprehensions (GH-160413) 
(#160457)

Adapt the reference-flag propagation from GH-156819 to fix gh-160299 without 
backporting the comprehensive changes to comprehension scope representation and 
compilation.

(cherry picked from commit cc3a0c9d3a260ed78c9751d40af4041bb826bf75)

files:
A Misc/NEWS.d/next/Library/2026-10-22-12-00-00.gh-issue-160299.CompUse.rst
M Lib/test/test_symtable.py
M Python/symtable.c

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,

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to