From 3d713c152b8f2b1cadcf0b65828107d1b40352fe Mon Sep 17 00:00:00 2001 From: Vyron Vasileiadis Date: Wed, 30 Sep 2026 13:28:31 +0300 Subject: [PATCH 1/3] gh-158490: Don't freeze a shared set in the frozenset() fast path make_frozenset() assumed that the set built for frozenset() of a set display or comprehension is uniquely referenced and froze it in place. A trace function or pdb can still hold the set returned by a comprehension that is not inlined, such as one in a class annotation. Copy the set in that case. --- Lib/test/test_builtin.py | 23 +++++++++++++++++++++++ Python/intrinsics.c | 4 +++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_builtin.py b/Lib/test/test_builtin.py index 294355698c8683..99cbd9df8476a1 100644 --- a/Lib/test/test_builtin.py +++ b/Lib/test/test_builtin.py @@ -329,6 +329,29 @@ async def f_set(): with self.assertRaises(TypeError): run_yielding_async_fn(f) + def test_frozenset_optimization_shared_set(self): + # gh-158490: frozenset({... for ...}) must not freeze a set that + # something else still references, here a trace function + kept = [] + def tracer(frame, event, arg): + if event == 'return' and type(arg) is set: + kept.append(arg) + return tracer + + class C: + x: frozenset({i for i in range(3)}) + + old_trace = sys.gettrace() + sys.settrace(tracer) + try: + fs = C.__annotations__['x'] + finally: + sys.settrace(old_trace) + self.assertEqual(fs, frozenset({0, 1, 2})) + self.assertEqual(len(kept), 1) + self.assertIs(type(kept[0]), set) + self.assertIsNot(kept[0], fs) + def test_ascii(self): self.assertEqual(ascii(''), '\'\'') self.assertEqual(ascii(0), '0') diff --git a/Python/intrinsics.c b/Python/intrinsics.c index dfa81baa654e12..06ec918f5bb28c 100644 --- a/Python/intrinsics.c +++ b/Python/intrinsics.c @@ -212,7 +212,9 @@ static PyObject * make_frozenset(PyThreadState* Py_UNUSED(ignored), PyObject *set) { assert(PySet_CheckExact(set)); - assert(_PyObject_IsUniquelyReferenced(set)); + if (!_PyObject_IsUniquelyReferenced(set)) { + return PyFrozenSet_New(set); + } return _PySet_Freeze(set); } From a39b8a509fa58d06fcd0374d0253b4fa5bbc6548 Mon Sep 17 00:00:00 2001 From: Vyron Vasileiadis Date: Tue, 3 Nov 2026 16:52:38 +0200 Subject: [PATCH 2/3] Address review: skip the fast path for a comprehension that is not inlined Instead of copying a shared set in make_frozenset(), don't use the frozenset() fast path for a set comprehension that is not inlined. Only such a comprehension returns its set from a call, where a trace function can keep it, so make_frozenset() keeps its assumption that the set is uniquely referenced and other uses pay for no extra check. --- Python/codegen.c | 14 ++++++++++++++ Python/intrinsics.c | 4 +--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Python/codegen.c b/Python/codegen.c index a975705576a167..f7b7be290f9957 100644 --- a/Python/codegen.c +++ b/Python/codegen.c @@ -4029,6 +4029,20 @@ maybe_optimize_function_call(compiler *c, expr_ty e, jump_target_label end) if (_PyUnicode_EqualToASCIIString(func->v.Name.id, "frozenset") && (arg_expr->kind == Set_kind || arg_expr->kind == SetComp_kind)) { + if (arg_expr->kind == SetComp_kind) { + /* A comprehension that is not inlined returns its set from a + call, so a trace function may still hold a reference to it. */ + PySTEntryObject *entry = _PySymtable_Lookup(SYMTABLE(c), + (void *)arg_expr); + if (entry == NULL) { + return ERROR; + } + int is_inlined = (entry->ste_type == InlinedComprehensionBlock); + Py_DECREF(entry); + if (!is_inlined) { + return 0; + } + } NEW_JUMP_TARGET_LABEL(c, skip_optimization); ADDOP_I(c, loc, COPY, 1); diff --git a/Python/intrinsics.c b/Python/intrinsics.c index 06ec918f5bb28c..dfa81baa654e12 100644 --- a/Python/intrinsics.c +++ b/Python/intrinsics.c @@ -212,9 +212,7 @@ static PyObject * make_frozenset(PyThreadState* Py_UNUSED(ignored), PyObject *set) { assert(PySet_CheckExact(set)); - if (!_PyObject_IsUniquelyReferenced(set)) { - return PyFrozenSet_New(set); - } + assert(_PyObject_IsUniquelyReferenced(set)); return _PySet_Freeze(set); } From e38e8e8165dd8bf944231d43a813d33f48e53737 Mon Sep 17 00:00:00 2001 From: Vyron Vasileiadis Date: Tue, 3 Nov 2026 17:53:21 +0200 Subject: [PATCH 3/3] Add a NEWS entry --- .../2026-11-03-17-53-21.gh-issue-158490.U63vs5.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-11-03-17-53-21.gh-issue-158490.U63vs5.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-11-03-17-53-21.gh-issue-158490.U63vs5.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-11-03-17-53-21.gh-issue-158490.U63vs5.rst new file mode 100644 index 00000000000000..80d28b10bf11ae --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-11-03-17-53-21.gh-issue-158490.U63vs5.rst @@ -0,0 +1,2 @@ +Fix :class:`frozenset` of a set comprehension in a class annotation or type +alias freezing a set that a trace function or debugger still references.