From a3292c221b806fb623c4dc8132939895cdc48eb2 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Thu, 16 Oct 2025 21:36:08 +0200 Subject: [PATCH 01/15] Do not track frozenset objects with immutables --- Objects/setobject.c | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/Objects/setobject.c b/Objects/setobject.c index 213bd821d8a1b9..a2d3985bc0bbbf 100644 --- a/Objects/setobject.c +++ b/Objects/setobject.c @@ -1174,6 +1174,23 @@ make_new_set_basetype(PyTypeObject *type, PyObject *iterable) return make_new_set(type, iterable); } +void +_PyFrozenSet_MaybeUntrack(PyObject *op) +{ + if ((op ==NULL) || !(PyFrozenSet_CheckExact(op))) { + return; + } + // the frozenset is tracked by the GC. if all elements are immutable we can untrack + Py_ssize_t pos = 0; + setentry *entry; + while (set_next((PySetObject *)op, &pos, &entry)) { + if (_PyObject_GC_MAY_BE_TRACKED(entry->key)) { + return; + } + } + _PyObject_GC_UNTRACK(op); +} + static PyObject * make_new_frozenset(PyTypeObject *type, PyObject *iterable) { @@ -1185,7 +1202,9 @@ make_new_frozenset(PyTypeObject *type, PyObject *iterable) /* frozenset(f) is idempotent */ return Py_NewRef(iterable); } - return make_new_set(type, iterable); + PyObject *obj = make_new_set(type, iterable); + _PyFrozenSet_MaybeUntrack(obj); + return obj; } static PyObject * @@ -2710,7 +2729,11 @@ PySet_New(PyObject *iterable) PyObject * PyFrozenSet_New(PyObject *iterable) { - return make_new_set(&PyFrozenSet_Type, iterable); + PyObject *result = make_new_set(&PyFrozenSet_Type, iterable); + if (result != NULL) { + _PyFrozenSet_MaybeUntrack(result); + } + return result; } Py_ssize_t From cd294a67f9f55059e1942fd988b36aa8f256158d Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Thu, 16 Oct 2025 23:00:19 +0200 Subject: [PATCH 02/15] cleanup --- Objects/setobject.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Objects/setobject.c b/Objects/setobject.c index a2d3985bc0bbbf..29cce311d92689 100644 --- a/Objects/setobject.c +++ b/Objects/setobject.c @@ -2730,9 +2730,7 @@ PyObject * PyFrozenSet_New(PyObject *iterable) { PyObject *result = make_new_set(&PyFrozenSet_Type, iterable); - if (result != NULL) { - _PyFrozenSet_MaybeUntrack(result); - } + _PyFrozenSet_MaybeUntrack(result); return result; } From 7e28cf2eb6a648a6db6eb774dbb1c7ef34bf70d8 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Thu, 16 Oct 2025 23:01:26 +0200 Subject: [PATCH 03/15] cleanup --- Objects/setobject.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Objects/setobject.c b/Objects/setobject.c index 29cce311d92689..f1fba0196802ff 100644 --- a/Objects/setobject.c +++ b/Objects/setobject.c @@ -1180,7 +1180,7 @@ _PyFrozenSet_MaybeUntrack(PyObject *op) if ((op ==NULL) || !(PyFrozenSet_CheckExact(op))) { return; } - // the frozenset is tracked by the GC. if all elements are immutable we can untrack + // if all elements of a frozenset are not tracked, we untrack the object Py_ssize_t pos = 0; setentry *entry; while (set_next((PySetObject *)op, &pos, &entry)) { From c4deb03b3af009b9feaffbd29304a4713dc7fb86 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Thu, 16 Oct 2025 23:20:11 +0200 Subject: [PATCH 04/15] fix test --- Lib/test/test_sys.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index 1198c6d35113c8..779f48750326d5 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -1876,7 +1876,10 @@ class S(set): check(S(), set(), '3P') class FS(frozenset): __slots__ = 'a', 'b', 'c' - check(FS(), frozenset(), '3P') + + class mytuple(tuple): + pass + check(FS([mytuple()]), frozenset([mytuple()]), '3P') from collections import OrderedDict class OD(OrderedDict): __slots__ = 'a', 'b', 'c' From 607237adc21fc916e5e89384ab6ca9e90ffb45db Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Thu, 16 Oct 2025 22:36:10 +0000 Subject: [PATCH 05/15] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20b?= =?UTF-8?q?lurb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2025-10-16-22-36-05.gh-issue-140232.u3srgv.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-10-16-22-36-05.gh-issue-140232.u3srgv.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-10-16-22-36-05.gh-issue-140232.u3srgv.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-16-22-36-05.gh-issue-140232.u3srgv.rst new file mode 100644 index 00000000000000..e40daacbc45b7b --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-16-22-36-05.gh-issue-140232.u3srgv.rst @@ -0,0 +1 @@ +Frozenset objects with immutable elements are no longer tracked by the garbage collector. From 2735a71d05ff2c8b63631b972d1400eebf0c7f37 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Fri, 17 Oct 2025 08:00:06 +0200 Subject: [PATCH 06/15] Update Objects/setobject.c Co-authored-by: Mikhail Efimov --- Objects/setobject.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Objects/setobject.c b/Objects/setobject.c index f1fba0196802ff..56ca0d5de0026a 100644 --- a/Objects/setobject.c +++ b/Objects/setobject.c @@ -1177,7 +1177,7 @@ make_new_set_basetype(PyTypeObject *type, PyObject *iterable) void _PyFrozenSet_MaybeUntrack(PyObject *op) { - if ((op ==NULL) || !(PyFrozenSet_CheckExact(op))) { + if ((op == NULL) || !(PyFrozenSet_CheckExact(op))) { return; } // if all elements of a frozenset are not tracked, we untrack the object From c05db549484c84cd10d2a60b3b0152634f8825c0 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Fri, 24 Oct 2025 12:41:05 +0200 Subject: [PATCH 07/15] make sure PySet_Add tracks frozensets if needed --- Modules/_testcapimodule.c | 63 ++++++++++++++++++++++++++++++++++++++- Objects/setobject.c | 3 ++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index 4e73be20e1b709..8d52a4eb5bddfd 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -2435,6 +2435,66 @@ test_critical_sections(PyObject *module, PyObject *Py_UNUSED(args)) } + +static PyObject * +test_pyset_add(PyObject* self, PyObject *Py_UNUSED(args)) +{ + + PyObject *set = NULL, *empty_tuple=NULL, *tracked_object; + + + tracked_object = PyImport_ImportModule("sys"); + if (tracked_object == NULL) { + goto failed; + } + if (!PyObject_GC_IsTracked(tracked_object)) { + PyErr_SetString(PyExc_ValueError, "Test item is not tracked by GC"); + goto failed; + } + + + int return_value; + empty_tuple = PyTuple_New(0); + if (empty_tuple == NULL) { + goto failed; + } + set = PyFrozenSet_New(empty_tuple); + if (set == NULL) { + return NULL; + } + + if (PyObject_GC_IsTracked(set)) { + PyErr_SetString(PyExc_ValueError, "Empty frozenset object is tracked by GC"); + goto failed; + } + return_value = PySet_Add(set, empty_tuple); + if (return_value<0) { + goto failed; + } + if (PyObject_GC_IsTracked(set)) { + PyErr_SetString(PyExc_ValueError, "Frozenset object with immutable is tracked by GC"); + goto failed; + } + + PySet_Add(set, tracked_object); + if (return_value<0) { + goto failed; + } + if (!PyObject_GC_IsTracked(set)) { + PyErr_SetString(PyExc_ValueError, "Frozenset object with tracked objects is not tracked by GC"); + goto failed; + } + + Py_RETURN_NONE; + +failed: + Py_XDECREF(tracked_object); + Py_XDECREF(empty_tuple); + Py_XDECREF(set); + return NULL; + +} + // Used by `finalize_thread_hang`. #if defined(_POSIX_THREADS) && !defined(__wasi__) static void finalize_thread_hang_cleanup_callback(void *Py_UNUSED(arg)) { @@ -2625,7 +2685,7 @@ static PyMethodDef TestMethods[] = { {"return_null_without_error", return_null_without_error, METH_NOARGS}, {"return_result_with_error", return_result_with_error, METH_NOARGS}, {"getitem_with_error", getitem_with_error, METH_VARARGS}, - {"Py_CompileString", pycompilestring, METH_O}, + {"Py_CompileString", pycompilestring, METH_O}, {"raise_SIGINT_then_send_None", raise_SIGINT_then_send_None, METH_VARARGS}, {"stack_pointer", stack_pointer, METH_NOARGS}, #ifdef W_STOPCODE @@ -2646,6 +2706,7 @@ static PyMethodDef TestMethods[] = { {"gen_get_code", gen_get_code, METH_O, NULL}, {"get_feature_macros", get_feature_macros, METH_NOARGS, NULL}, {"test_code_api", test_code_api, METH_NOARGS, NULL}, + {"test_pyset_add", test_pyset_add, METH_NOARGS, NULL}, {"settrace_to_error", settrace_to_error, METH_O, NULL}, {"settrace_to_record", settrace_to_record, METH_O, NULL}, {"test_macros", test_macros, METH_NOARGS, NULL}, diff --git a/Objects/setobject.c b/Objects/setobject.c index a2d3985bc0bbbf..729a8f182908f6 100644 --- a/Objects/setobject.c +++ b/Objects/setobject.c @@ -2802,6 +2802,9 @@ PySet_Add(PyObject *anyset, PyObject *key) return -1; } + if (PyFrozenSet_Check(anyset) && PyObject_GC_IsTracked(key) && !PyObject_GC_IsTracked(anyset) ) { + _PyObject_GC_TRACK(anyset); + } int rv; Py_BEGIN_CRITICAL_SECTION(anyset); rv = set_add_key((PySetObject *)anyset, key); From 0b97604bcd2e3c2e9595921115b5a95cda12a448 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Fri, 24 Oct 2025 14:30:30 +0200 Subject: [PATCH 08/15] review comment --- Modules/_testcapimodule.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index 8d52a4eb5bddfd..739a4cf1c9f205 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -2441,7 +2441,7 @@ test_pyset_add(PyObject* self, PyObject *Py_UNUSED(args)) { PyObject *set = NULL, *empty_tuple=NULL, *tracked_object; - + int return_value; tracked_object = PyImport_ImportModule("sys"); if (tracked_object == NULL) { @@ -2452,15 +2452,13 @@ test_pyset_add(PyObject* self, PyObject *Py_UNUSED(args)) goto failed; } - - int return_value; empty_tuple = PyTuple_New(0); if (empty_tuple == NULL) { goto failed; } set = PyFrozenSet_New(empty_tuple); if (set == NULL) { - return NULL; + goto failed; } if (PyObject_GC_IsTracked(set)) { From 08e22c30a6268d77cfc769ff2cd21daba62155a6 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Sun, 26 Oct 2025 20:18:04 +0100 Subject: [PATCH 09/15] use _testcapi for testing --- Lib/test/test_set.py | 26 ++++++++++++++++ Modules/_testcapimodule.c | 63 ++++++++------------------------------- 2 files changed, 39 insertions(+), 50 deletions(-) diff --git a/Lib/test/test_set.py b/Lib/test/test_set.py index c0df9507bd7f5e..10a152759ee693 100644 --- a/Lib/test/test_set.py +++ b/Lib/test/test_set.py @@ -9,6 +9,7 @@ import warnings import weakref from random import randrange, shuffle +import _testcapi from test import support from test.support import warnings_helper @@ -2154,6 +2155,31 @@ def test_cuboctahedron(self): for cubevert in edge: self.assertIn(cubevert, g) +class TestPySet_Add(unittest.TestCase): + def test_set(self): + # Test the PySet_Add c-api for set objects + s = set() + assert _testcapi.pyset_add(s, 1) == {1} + self.assertRaises(TypeError, _testcapi.pyset_add, s, []) + + def test_frozenset(self): + # Test the PySet_Add c-api for fronzetset objects + + assert _testcapi.pyset_add(frozenset(), 1) == frozenset([1]) + frozen_set = frozenset() + self.assertRaises(SystemError, _testcapi.pyset_add, frozen_set, 1) + + def test_frozenset_gc_tracking(self): + # see gh-140234 + class TrackedHashableClass(): + pass + + a = TrackedHashableClass() + result_set = _testcapi.pyset_add(frozenset(), 1) + assert not gc.is_tracked(result_set) + result_set = _testcapi.pyset_add(frozenset(), a) + assert gc.is_tracked(result_set) + #============================================================================== diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index 739a4cf1c9f205..6ccb28c185f97d 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -2437,60 +2437,23 @@ test_critical_sections(PyObject *module, PyObject *Py_UNUSED(args)) static PyObject * -test_pyset_add(PyObject* self, PyObject *Py_UNUSED(args)) +// Interface to pyset_add, returning the set +pyset_add(PyObject* self, PyObject* const* args, Py_ssize_t nargsf) { - - PyObject *set = NULL, *empty_tuple=NULL, *tracked_object; - int return_value; - - tracked_object = PyImport_ImportModule("sys"); - if (tracked_object == NULL) { - goto failed; - } - if (!PyObject_GC_IsTracked(tracked_object)) { - PyErr_SetString(PyExc_ValueError, "Test item is not tracked by GC"); - goto failed; - } - - empty_tuple = PyTuple_New(0); - if (empty_tuple == NULL) { - goto failed; - } - set = PyFrozenSet_New(empty_tuple); - if (set == NULL) { - goto failed; - } - - if (PyObject_GC_IsTracked(set)) { - PyErr_SetString(PyExc_ValueError, "Empty frozenset object is tracked by GC"); - goto failed; - } - return_value = PySet_Add(set, empty_tuple); - if (return_value<0) { - goto failed; - } - if (PyObject_GC_IsTracked(set)) { - PyErr_SetString(PyExc_ValueError, "Frozenset object with immutable is tracked by GC"); - goto failed; + Py_ssize_t nargs = _PyVectorcall_NARGS(nargsf); + if (nargs != 2) { + PyErr_SetString(PyExc_ValueError, "pyset_add requires exactly two arguments"); + return NULL; } + PyObject *set = args[0]; + PyObject *item = args[1]; - PySet_Add(set, tracked_object); - if (return_value<0) { - goto failed; - } - if (!PyObject_GC_IsTracked(set)) { - PyErr_SetString(PyExc_ValueError, "Frozenset object with tracked objects is not tracked by GC"); - goto failed; + int return_value = PySet_Add(set, item); + if (return_value < 0) { + return NULL; } - Py_RETURN_NONE; - -failed: - Py_XDECREF(tracked_object); - Py_XDECREF(empty_tuple); - Py_XDECREF(set); - return NULL; - + return Py_NewRef(set); } // Used by `finalize_thread_hang`. @@ -2704,7 +2667,7 @@ static PyMethodDef TestMethods[] = { {"gen_get_code", gen_get_code, METH_O, NULL}, {"get_feature_macros", get_feature_macros, METH_NOARGS, NULL}, {"test_code_api", test_code_api, METH_NOARGS, NULL}, - {"test_pyset_add", test_pyset_add, METH_NOARGS, NULL}, + {"pyset_add", _PyCFunction_CAST(pyset_add), METH_FASTCALL, NULL}, {"settrace_to_error", settrace_to_error, METH_O, NULL}, {"settrace_to_record", settrace_to_record, METH_O, NULL}, {"test_macros", test_macros, METH_NOARGS, NULL}, From 62afc766180c7f877c96e0c2becd71548cc81e60 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Sun, 26 Oct 2025 20:24:24 +0100 Subject: [PATCH 10/15] whitespace --- Lib/test/test_set.py | 1 - Modules/_testcapimodule.c | 2 +- Objects/setobject.c | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_set.py b/Lib/test/test_set.py index 10a152759ee693..ef86ead324111f 100644 --- a/Lib/test/test_set.py +++ b/Lib/test/test_set.py @@ -2164,7 +2164,6 @@ def test_set(self): def test_frozenset(self): # Test the PySet_Add c-api for fronzetset objects - assert _testcapi.pyset_add(frozenset(), 1) == frozenset([1]) frozen_set = frozenset() self.assertRaises(SystemError, _testcapi.pyset_add, frozen_set, 1) diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index 6ccb28c185f97d..0cd70cee4692d4 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -2437,7 +2437,7 @@ test_critical_sections(PyObject *module, PyObject *Py_UNUSED(args)) static PyObject * -// Interface to pyset_add, returning the set +// Interface to PySet_Add, returning the set pyset_add(PyObject* self, PyObject* const* args, Py_ssize_t nargsf) { Py_ssize_t nargs = _PyVectorcall_NARGS(nargsf); diff --git a/Objects/setobject.c b/Objects/setobject.c index 8e89e637881bd1..cf3abccf558569 100644 --- a/Objects/setobject.c +++ b/Objects/setobject.c @@ -1180,7 +1180,7 @@ _PyFrozenSet_MaybeUntrack(PyObject *op) if ((op == NULL) || !(PyFrozenSet_CheckExact(op))) { return; } - // if all elements of a frozenset are not tracked, we untrack the object + // if no elements of a frozenset are tracked, we untrack the object Py_ssize_t pos = 0; setentry *entry; while (set_next((PySetObject *)op, &pos, &entry)) { From 37fc61d77ad098376d9cb5c09ba46db26e7139bd Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Sun, 26 Oct 2025 21:35:14 +0100 Subject: [PATCH 11/15] Apply suggestions from code review Co-authored-by: Mikhail Efimov --- Lib/test/test_set.py | 2 +- Objects/setobject.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_set.py b/Lib/test/test_set.py index ef86ead324111f..dc93bdfbceabb4 100644 --- a/Lib/test/test_set.py +++ b/Lib/test/test_set.py @@ -2163,7 +2163,7 @@ def test_set(self): self.assertRaises(TypeError, _testcapi.pyset_add, s, []) def test_frozenset(self): - # Test the PySet_Add c-api for fronzetset objects + # Test the PySet_Add c-api for frozenset objects assert _testcapi.pyset_add(frozenset(), 1) == frozenset([1]) frozen_set = frozenset() self.assertRaises(SystemError, _testcapi.pyset_add, frozen_set, 1) diff --git a/Objects/setobject.c b/Objects/setobject.c index cf3abccf558569..ea49faacbf0255 100644 --- a/Objects/setobject.c +++ b/Objects/setobject.c @@ -1177,7 +1177,7 @@ make_new_set_basetype(PyTypeObject *type, PyObject *iterable) void _PyFrozenSet_MaybeUntrack(PyObject *op) { - if ((op == NULL) || !(PyFrozenSet_CheckExact(op))) { + if (op == NULL || !PyFrozenSet_CheckExact(op)) { return; } // if no elements of a frozenset are tracked, we untrack the object From 4f8bda77afd1ef008fb874f82dd0e6ae4c550762 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Sun, 26 Oct 2025 21:42:23 +0100 Subject: [PATCH 12/15] review comment --- Objects/setobject.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Objects/setobject.c b/Objects/setobject.c index cf3abccf558569..8f89252ed268f1 100644 --- a/Objects/setobject.c +++ b/Objects/setobject.c @@ -2800,7 +2800,7 @@ PySet_Add(PyObject *anyset, PyObject *key) return -1; } - if (PyFrozenSet_Check(anyset) && PyObject_GC_IsTracked(key) && !PyObject_GC_IsTracked(anyset) ) { + if (PyFrozenSet_CheckExact(anyset) && PyObject_GC_IsTracked(key) && !PyObject_GC_IsTracked(anyset) ) { _PyObject_GC_TRACK(anyset); } int rv; From 4b39149cbec2f07d5e4fe569d42affdf07ff41b7 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Tue, 28 Oct 2025 21:57:59 +0100 Subject: [PATCH 13/15] Apply suggestions from code review Co-authored-by: Victor Stinner --- Lib/test/test_set.py | 6 +++--- Modules/_testcapimodule.c | 1 - Objects/setobject.c | 6 ++++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_set.py b/Lib/test/test_set.py index dc93bdfbceabb4..779a30d6d93dbf 100644 --- a/Lib/test/test_set.py +++ b/Lib/test/test_set.py @@ -2159,7 +2159,7 @@ class TestPySet_Add(unittest.TestCase): def test_set(self): # Test the PySet_Add c-api for set objects s = set() - assert _testcapi.pyset_add(s, 1) == {1} + self.assertEqual(_testcapi.pyset_add(s, 1), {1}) self.assertRaises(TypeError, _testcapi.pyset_add, s, []) def test_frozenset(self): @@ -2175,9 +2175,9 @@ class TrackedHashableClass(): a = TrackedHashableClass() result_set = _testcapi.pyset_add(frozenset(), 1) - assert not gc.is_tracked(result_set) + self.assertFalse(gc.is_tracked(result_set)) result_set = _testcapi.pyset_add(frozenset(), a) - assert gc.is_tracked(result_set) + self.assertTrue(gc.is_tracked(result_set)) #============================================================================== diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index 0cd70cee4692d4..176d807c586305 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -2452,7 +2452,6 @@ pyset_add(PyObject* self, PyObject* const* args, Py_ssize_t nargsf) if (return_value < 0) { return NULL; } - return Py_NewRef(set); } diff --git a/Objects/setobject.c b/Objects/setobject.c index 8a189e56507190..618b739a61b5c2 100644 --- a/Objects/setobject.c +++ b/Objects/setobject.c @@ -1180,7 +1180,7 @@ _PyFrozenSet_MaybeUntrack(PyObject *op) if (op == NULL || !PyFrozenSet_CheckExact(op)) { return; } - // if no elements of a frozenset are tracked, we untrack the object + // if no elements of a frozenset are tracked by the GC, we untrack the object Py_ssize_t pos = 0; setentry *entry; while (set_next((PySetObject *)op, &pos, &entry)) { @@ -1203,7 +1203,9 @@ make_new_frozenset(PyTypeObject *type, PyObject *iterable) return Py_NewRef(iterable); } PyObject *obj = make_new_set(type, iterable); - _PyFrozenSet_MaybeUntrack(obj); + if (obj != NULL) { + _PyFrozenSet_MaybeUntrack(obj); + } return obj; } From 28598021f67bc20effea7d7754930c13f42e5838 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Tue, 28 Oct 2025 22:09:42 +0100 Subject: [PATCH 14/15] review comments --- Lib/test/test_set.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_set.py b/Lib/test/test_set.py index 779a30d6d93dbf..9e4c45c840ff5e 100644 --- a/Lib/test/test_set.py +++ b/Lib/test/test_set.py @@ -2164,8 +2164,9 @@ def test_set(self): def test_frozenset(self): # Test the PySet_Add c-api for frozenset objects - assert _testcapi.pyset_add(frozenset(), 1) == frozenset([1]) + self.assertEqual(_testcapi.pyset_add(frozenset(), 1), frozenset([1])) frozen_set = frozenset() + # if the argument to PySet_Add is a frozenset that is not uniquely references an error is generated self.assertRaises(SystemError, _testcapi.pyset_add, frozen_set, 1) def test_frozenset_gc_tracking(self): From 08f43c514f7a0ec92391a70c1ab15b080a49f5fc Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Tue, 28 Oct 2025 22:18:01 +0100 Subject: [PATCH 15/15] review comments --- Objects/setobject.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Objects/setobject.c b/Objects/setobject.c index 618b739a61b5c2..60542a06e69323 100644 --- a/Objects/setobject.c +++ b/Objects/setobject.c @@ -1175,9 +1175,12 @@ make_new_set_basetype(PyTypeObject *type, PyObject *iterable) } void +// gh-140232: check whether a frozenset can be untracked from the GC _PyFrozenSet_MaybeUntrack(PyObject *op) { - if (op == NULL || !PyFrozenSet_CheckExact(op)) { + assert(op != NULL); + // subclasses of a frozenset can generate reference cycles, so do not untrack + if (!PyFrozenSet_CheckExact(op)) { return; } // if no elements of a frozenset are tracked by the GC, we untrack the object @@ -2732,7 +2735,9 @@ PyObject * PyFrozenSet_New(PyObject *iterable) { PyObject *result = make_new_set(&PyFrozenSet_Type, iterable); - _PyFrozenSet_MaybeUntrack(result); + if (result != 0) { + _PyFrozenSet_MaybeUntrack(result); + } return result; }