From a3292c221b806fb623c4dc8132939895cdc48eb2 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Thu, 16 Oct 2025 21:36:08 +0200 Subject: [PATCH 1/6] 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 2/6] 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 3/6] 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 4/6] 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 5/6] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=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 6/6] 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