From 255b3219ccdccd6ed9c29029bb5009cca27968a9 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 24 Jun 2026 13:11:51 +0200 Subject: [PATCH 1/9] gh-151722: Do not track the dict in the GC in _PyDict_FromKeys() dict_merge() no longer requires the dictionary to be tracked by the GC. Co-authored-by: Donghee Na --- ...-06-24-13-36-31.gh-issue-151722.lWKfE1.rst | 3 + Objects/dictobject.c | 84 ++++++++++++------- 2 files changed, 57 insertions(+), 30 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-06-24-13-36-31.gh-issue-151722.lWKfE1.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-06-24-13-36-31.gh-issue-151722.lWKfE1.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-24-13-36-31.gh-issue-151722.lWKfE1.rst new file mode 100644 index 00000000000000..6cea69523a2082 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-24-13-36-31.gh-issue-151722.lWKfE1.rst @@ -0,0 +1,3 @@ +:meth:`!frozendict.fromkeys` now creates a :class:`frozendict` which is not +tracked by the garbage collector, and only tracks it once the dictionary is +fully initialized. Patch by Donghee Na and Victor Stinner. diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 9210398ee551de..54a3d1513d40ab 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -140,6 +140,7 @@ static PyObject* frozendict_new(PyTypeObject *type, PyObject *args, PyObject *kwds); static PyObject* frozendict_new_untracked(PyTypeObject *type); static PyObject* dict_new(PyTypeObject *type, PyObject *args, PyObject *kwds); +static PyObject* dict_new_untracked(PyTypeObject *type); static int dict_merge(PyObject *a, PyObject *b, int override, PyObject **dupkey); static int dict_contains(PyObject *op, PyObject *key); static int dict_merge_from_seq2(PyObject *d, PyObject *seq2, int override); @@ -3414,40 +3415,49 @@ dict_set_fromkeys(PyDictObject *mp, PyObject *iterable, PyObject *value) PyObject * _PyDict_FromKeys(PyObject *cls, PyObject *iterable, PyObject *value) { - PyObject *it; /* iter(iterable) */ + PyObject *it = NULL; /* iter(iterable) */ PyObject *key; PyObject *d; int status; - d = _PyObject_CallNoArgs(cls); + PyTypeObject *cls_type = _PyType_CAST(cls); + if (PyObject_IsSubclass(cls, (PyObject*)&PyFrozenDict_Type) + && cls_type->tp_new == frozendict_new) + { + // gh-151722: Create a frozendict copy which is not tracked by the GC. + d = frozendict_new_untracked(cls_type); + } + else { + d = _PyObject_CallNoArgs(cls); + } if (d == NULL) { return NULL; } - // If cls is a dict or frozendict subclass with overridden constructor, - // copy the frozendict. - PyTypeObject *cls_type = _PyType_CAST(cls); - if (PyFrozenDict_Check(d) && cls_type->tp_new != frozendict_new) { + // gh-151722: If cls constructor returns a frozendict which is tracked by + // the GC, create a frozendict copy which is not tracked by the GC. + if (PyFrozenDict_Check(d) && _PyObject_GC_IS_TRACKED(d)) { // Subclass-friendly copy PyObject *copy; if (PyObject_IsSubclass(cls, (PyObject*)&PyFrozenDict_Type)) { - copy = frozendict_new(cls_type, NULL, NULL); + copy = frozendict_new_untracked(cls_type); } else { - copy = dict_new(cls_type, NULL, NULL); + copy = dict_new_untracked(cls_type); } if (copy == NULL) { - Py_DECREF(d); - return NULL; + goto Fail; } if (dict_merge(copy, d, 1, NULL) < 0) { - Py_DECREF(d); Py_DECREF(copy); - return NULL; + goto Fail; } Py_SETREF(d, copy); } assert(!PyFrozenDict_Check(d) || can_modify_dict((PyDictObject*)d)); + if (PyFrozenDict_Check(d)) { + assert(!_PyObject_GC_IS_TRACKED(d)); + } if (PyDict_CheckExact(d)) { if (PyDict_CheckExact(iterable)) { @@ -3456,7 +3466,7 @@ _PyDict_FromKeys(PyObject *cls, PyObject *iterable, PyObject *value) Py_BEGIN_CRITICAL_SECTION2(d, iterable); d = (PyObject *)dict_dict_fromkeys(mp, iterable, value); Py_END_CRITICAL_SECTION2(); - return d; + goto Done; } else if (PyFrozenDict_CheckExact(iterable)) { PyDictObject *mp = (PyDictObject *)d; @@ -3464,7 +3474,7 @@ _PyDict_FromKeys(PyObject *cls, PyObject *iterable, PyObject *value) Py_BEGIN_CRITICAL_SECTION(d); d = (PyObject *)dict_dict_fromkeys(mp, iterable, value); Py_END_CRITICAL_SECTION(); - return d; + goto Done; } else if (PyAnySet_CheckExact(iterable)) { PyDictObject *mp = (PyDictObject *)d; @@ -3472,7 +3482,7 @@ _PyDict_FromKeys(PyObject *cls, PyObject *iterable, PyObject *value) Py_BEGIN_CRITICAL_SECTION2(d, iterable); d = (PyObject *)dict_set_fromkeys(mp, iterable, value); Py_END_CRITICAL_SECTION2(); - return d; + goto Done; } } else if (PyFrozenDict_CheckExact(d)) { @@ -3482,12 +3492,12 @@ _PyDict_FromKeys(PyObject *cls, PyObject *iterable, PyObject *value) Py_BEGIN_CRITICAL_SECTION(iterable); d = (PyObject *)dict_dict_fromkeys(mp, iterable, value); Py_END_CRITICAL_SECTION(); - return d; + goto Done; } else if (PyFrozenDict_CheckExact(iterable)) { PyDictObject *mp = (PyDictObject *)d; d = (PyObject *)dict_dict_fromkeys(mp, iterable, value); - return d; + goto Done; } else if (PyAnySet_CheckExact(iterable)) { PyDictObject *mp = (PyDictObject *)d; @@ -3495,14 +3505,13 @@ _PyDict_FromKeys(PyObject *cls, PyObject *iterable, PyObject *value) Py_BEGIN_CRITICAL_SECTION(iterable); d = (PyObject *)dict_set_fromkeys(mp, iterable, value); Py_END_CRITICAL_SECTION(); - return d; + goto Done; } } it = PyObject_GetIter(iterable); if (it == NULL){ - Py_DECREF(d); - return NULL; + goto Fail; } if (PyDict_CheckExact(d)) { @@ -3541,12 +3550,19 @@ dict_iter_exit:; if (PyErr_Occurred()) goto Fail; Py_DECREF(it); - return d; + goto Done; Fail: - Py_DECREF(it); + Py_XDECREF(it); Py_DECREF(d); return NULL; + +Done: + // d can be NULL + if (d != NULL && !_PyObject_GC_IS_TRACKED(d)) { + _PyObject_GC_TRACK(d); + } + return d; } /* Methods */ @@ -4147,9 +4163,6 @@ dict_dict_merge(PyDictObject *mp, PyDictObject *other, int override, PyObject ** set_keys(mp, keys); STORE_USED(mp, other->ma_used); ASSERT_CONSISTENT(mp); - if (PyDict_Check(mp)) { - assert(_PyObject_GC_IS_TRACKED(mp)); - } return 0; } } @@ -4316,7 +4329,12 @@ dict_merge_api(PyObject *a, PyObject *b, int override, PyObject **dupkey) } return -1; } - return dict_merge(a, b, override, dupkey); + + int res = dict_merge(a, b, override, dupkey); + if (PyDict_Check(a)) { + assert(_PyObject_GC_IS_TRACKED(a)); + } + return res; } int @@ -4475,10 +4493,16 @@ copy_lock_held(PyObject *o, int as_frozendict) } if (copy == NULL) return NULL; - if (dict_merge(copy, o, 1, NULL) == 0) - return copy; - Py_DECREF(copy); - return NULL; + if (dict_merge(copy, o, 1, NULL) < 0) { + Py_DECREF(copy); + return NULL; + + } + + if (PyDict_Check(copy)) { + assert(_PyObject_GC_IS_TRACKED(copy)); + } + return copy; } PyObject * From a7726f2026c6fb2cb0560b81bd61b5bc162f74d4 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 24 Jun 2026 22:05:20 +0200 Subject: [PATCH 2/9] Use Py_TYPE(d) instead of cls_type to create the copy --- Lib/test/test_dict.py | 22 +++++++++++----------- Objects/dictobject.c | 8 +------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/Lib/test/test_dict.py b/Lib/test/test_dict.py index f26586809238f0..0ac0e97ca1cec4 100644 --- a/Lib/test/test_dict.py +++ b/Lib/test/test_dict.py @@ -1944,22 +1944,14 @@ def __new__(self): fd = FrozenDictSubclass.fromkeys("abc") self.assertEqual(fd, frozendict(x=1, a=None, b=None, c=None)) - self.assertEqual(type(fd), FrozenDictSubclass) + self.assertEqual(type(fd), frozendict) self.assertEqual(created, frozendict(x=1)) fd = FrozenDictSubclass.fromkeys(frozendict(y=2)) self.assertEqual(fd, frozendict(x=1, y=None)) - self.assertEqual(type(fd), FrozenDictSubclass) + self.assertEqual(type(fd), frozendict) self.assertEqual(created, frozendict(x=1)) - # Subclass which doesn't override the constructor - class FrozenDictSubclass2(frozendict): - pass - - fd = FrozenDictSubclass2.fromkeys("abc") - self.assertEqual(fd, frozendict(a=None, b=None, c=None)) - self.assertEqual(type(fd), FrozenDictSubclass2) - # Dict subclass which overrides the constructor class DictSubclass(dict): def __new__(self): @@ -1967,9 +1959,17 @@ def __new__(self): fd = DictSubclass.fromkeys("abc") self.assertEqual(fd, frozendict(x=1, a=None, b=None, c=None)) - self.assertEqual(type(fd), DictSubclass) + self.assertEqual(type(fd), frozendict) self.assertEqual(created, frozendict(x=1)) + # Subclass which doesn't override the constructor + class FrozenDictSubclass2(frozendict): + pass + + fd = FrozenDictSubclass2.fromkeys("abc") + self.assertEqual(fd, frozendict(a=None, b=None, c=None)) + self.assertEqual(type(fd), FrozenDictSubclass2) + def test_pickle(self): for proto in range(pickle.HIGHEST_PROTOCOL + 1): for fd in ( diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 54a3d1513d40ab..92cc592dae4658 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -3438,13 +3438,7 @@ _PyDict_FromKeys(PyObject *cls, PyObject *iterable, PyObject *value) // the GC, create a frozendict copy which is not tracked by the GC. if (PyFrozenDict_Check(d) && _PyObject_GC_IS_TRACKED(d)) { // Subclass-friendly copy - PyObject *copy; - if (PyObject_IsSubclass(cls, (PyObject*)&PyFrozenDict_Type)) { - copy = frozendict_new_untracked(cls_type); - } - else { - copy = dict_new_untracked(cls_type); - } + PyObject *copy = frozendict_new_untracked(Py_TYPE(d)); if (copy == NULL) { goto Fail; } From 79528affa1fd4eabbc27a31d7cf0061c334556b7 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 24 Jun 2026 22:23:05 +0200 Subject: [PATCH 3/9] Rephrase the NEWS entry --- .../2026-06-24-13-36-31.gh-issue-151722.lWKfE1.rst | 6 +++--- Objects/dictobject.c | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-06-24-13-36-31.gh-issue-151722.lWKfE1.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-24-13-36-31.gh-issue-151722.lWKfE1.rst index 6cea69523a2082..db50b0058bdbdd 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2026-06-24-13-36-31.gh-issue-151722.lWKfE1.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-24-13-36-31.gh-issue-151722.lWKfE1.rst @@ -1,3 +1,3 @@ -:meth:`!frozendict.fromkeys` now creates a :class:`frozendict` which is not -tracked by the garbage collector, and only tracks it once the dictionary is -fully initialized. Patch by Donghee Na and Victor Stinner. +:meth:`!frozendict.fromkeys` now only tracks the :class:`frozendict` in the +garbage collector once the dictionary is fully initialized. Patch by Donghee Na +and Victor Stinner. diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 92cc592dae4658..369ee4027390b7 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -3436,6 +3436,9 @@ _PyDict_FromKeys(PyObject *cls, PyObject *iterable, PyObject *value) // gh-151722: If cls constructor returns a frozendict which is tracked by // the GC, create a frozendict copy which is not tracked by the GC. + // + // Untracking the dictionary requires tracking again the dictionary on + // error which is more complicated. It's easier to work on a copy. if (PyFrozenDict_Check(d) && _PyObject_GC_IS_TRACKED(d)) { // Subclass-friendly copy PyObject *copy = frozendict_new_untracked(Py_TYPE(d)); @@ -3448,8 +3451,8 @@ _PyDict_FromKeys(PyObject *cls, PyObject *iterable, PyObject *value) } Py_SETREF(d, copy); } - assert(!PyFrozenDict_Check(d) || can_modify_dict((PyDictObject*)d)); if (PyFrozenDict_Check(d)) { + assert(can_modify_dict((PyDictObject*)d)); assert(!_PyObject_GC_IS_TRACKED(d)); } From 3b241919f2f1342a8d220c5e019fb63ad707a782 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 25 Jun 2026 15:51:47 +0200 Subject: [PATCH 4/9] Call cls(fd) if working on a copy --- Lib/test/test_dict.py | 20 +++++++++++++------- Objects/dictobject.c | 23 +++++++++++++++++------ 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/Lib/test/test_dict.py b/Lib/test/test_dict.py index 0ac0e97ca1cec4..464292374d8f94 100644 --- a/Lib/test/test_dict.py +++ b/Lib/test/test_dict.py @@ -1939,27 +1939,33 @@ def test_fromkeys(self): # Subclass which overrides the constructor created = frozendict(x=1) class FrozenDictSubclass(frozendict): - def __new__(self): - return created + def __new__(cls, *args, **kwargs): + if args or kwargs: + return super().__new__(cls, *args, **kwargs) + else: + return created fd = FrozenDictSubclass.fromkeys("abc") self.assertEqual(fd, frozendict(x=1, a=None, b=None, c=None)) - self.assertEqual(type(fd), frozendict) + self.assertEqual(type(fd), FrozenDictSubclass) self.assertEqual(created, frozendict(x=1)) fd = FrozenDictSubclass.fromkeys(frozendict(y=2)) self.assertEqual(fd, frozendict(x=1, y=None)) - self.assertEqual(type(fd), frozendict) + self.assertEqual(type(fd), FrozenDictSubclass) self.assertEqual(created, frozendict(x=1)) # Dict subclass which overrides the constructor class DictSubclass(dict): - def __new__(self): - return created + def __new__(cls, *args, **kwargs): + if args or kwargs: + return super().__new__(cls, *args, **kwargs) + else: + return created fd = DictSubclass.fromkeys("abc") self.assertEqual(fd, frozendict(x=1, a=None, b=None, c=None)) - self.assertEqual(type(fd), frozendict) + self.assertEqual(type(fd), DictSubclass) self.assertEqual(created, frozendict(x=1)) # Subclass which doesn't override the constructor diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 369ee4027390b7..adb86344805457 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -3419,12 +3419,13 @@ _PyDict_FromKeys(PyObject *cls, PyObject *iterable, PyObject *value) PyObject *key; PyObject *d; int status; + int need_copy = 0; PyTypeObject *cls_type = _PyType_CAST(cls); if (PyObject_IsSubclass(cls, (PyObject*)&PyFrozenDict_Type) && cls_type->tp_new == frozendict_new) { - // gh-151722: Create a frozendict copy which is not tracked by the GC. + // gh-151722: Create a frozendict which is not tracked by the GC. d = frozendict_new_untracked(cls_type); } else { @@ -3437,11 +3438,14 @@ _PyDict_FromKeys(PyObject *cls, PyObject *iterable, PyObject *value) // gh-151722: If cls constructor returns a frozendict which is tracked by // the GC, create a frozendict copy which is not tracked by the GC. // - // Untracking the dictionary requires tracking again the dictionary on + // At the function exit, return cls(fd) where fd is a frozendict. + // + // Untracking the frozendict requires tracking again the frozendict on // error which is more complicated. It's easier to work on a copy. if (PyFrozenDict_Check(d) && _PyObject_GC_IS_TRACKED(d)) { - // Subclass-friendly copy - PyObject *copy = frozendict_new_untracked(Py_TYPE(d)); + need_copy = 1; + + PyObject *copy = frozendict_new_untracked(&PyFrozenDict_Type); if (copy == NULL) { goto Fail; } @@ -3555,8 +3559,15 @@ dict_iter_exit:; return NULL; Done: - // d can be NULL - if (d != NULL && !_PyObject_GC_IS_TRACKED(d)) { + if (d == NULL) { + return NULL; + } + + if (need_copy) { + PyObject *copy = _PyObject_CallOneArg(cls, d); + Py_SETREF(d, copy); + } + else if (!_PyObject_GC_IS_TRACKED(d)) { _PyObject_GC_TRACK(d); } return d; From 4304c9e0ecc876d025ab6255bfe56bfeafa41484 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 25 Jun 2026 15:57:03 +0200 Subject: [PATCH 5/9] Add assertion to frozendict_new_untracked() --- Objects/dictobject.c | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/Objects/dictobject.c b/Objects/dictobject.c index adb86344805457..1efa2cbbe31d1d 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -5271,11 +5271,11 @@ static PyNumberMethods dict_as_number = { .nb_inplace_or = _PyDict_IOr, }; -static PyObject * -dict_new_untracked(PyTypeObject *type) +static PyObject* +anydict_new_untracked(PyTypeObject *type) { assert(type != NULL); - // dict subclasses must implement the GC protocol + // dict and frozendict subclasses must implement the GC protocol assert(_PyType_IS_GC(type)); PyObject *self = _PyType_AllocNoTrack(type, 0); @@ -5294,6 +5294,14 @@ dict_new_untracked(PyTypeObject *type) return self; } +static PyObject* +dict_new_untracked(PyTypeObject *type) +{ + assert(PyObject_IsSubclass((PyObject*)type, (PyObject*)&PyDict_Type)); + + return anydict_new_untracked(type); +} + static PyObject * dict_new(PyTypeObject *type, PyObject *Py_UNUSED(args), PyObject *Py_UNUSED(kwds)) { @@ -8409,7 +8417,9 @@ frozendict_hash(PyObject *op) static PyObject * frozendict_new_untracked(PyTypeObject *type) { - PyObject *d = dict_new_untracked(type); + assert(PyObject_IsSubclass((PyObject*)type, (PyObject*)&PyFrozenDict_Type)); + + PyObject *d = anydict_new_untracked(type); if (d == NULL) { return NULL; } From 5aad887b43062c131b1ff5d5101bed9d3a5b635d Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 25 Jun 2026 15:52:07 +0200 Subject: [PATCH 6/9] Cleanup error path --- Objects/dictobject.c | 53 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 1efa2cbbe31d1d..2f4d7cb0c0d214 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -3416,9 +3416,7 @@ PyObject * _PyDict_FromKeys(PyObject *cls, PyObject *iterable, PyObject *value) { PyObject *it = NULL; /* iter(iterable) */ - PyObject *key; PyObject *d; - int status; int need_copy = 0; PyTypeObject *cls_type = _PyType_CAST(cls); @@ -3429,6 +3427,8 @@ _PyDict_FromKeys(PyObject *cls, PyObject *iterable, PyObject *value) d = frozendict_new_untracked(cls_type); } else { + // Dict subclass, or frozendict subclass which overrides + // the constructor. d = _PyObject_CallNoArgs(cls); } if (d == NULL) { @@ -3516,44 +3516,73 @@ _PyDict_FromKeys(PyObject *cls, PyObject *iterable, PyObject *value) } if (PyDict_CheckExact(d)) { + int status = 0; + Py_BEGIN_CRITICAL_SECTION(d); - while ((key = PyIter_Next(it)) != NULL) { + while (1) { + PyObject *key; + status = PyIter_NextItem(it, &key); + if (status <= 0) { + break; + } + status = setitem_lock_held((PyDictObject *)d, key, value); Py_DECREF(key); if (status < 0) { - assert(PyErr_Occurred()); - goto dict_iter_exit; + break; } } -dict_iter_exit:; Py_END_CRITICAL_SECTION(); + + if (status < 0) { + goto Fail; + } } else if (PyFrozenDict_Check(d)) { - while ((key = PyIter_Next(it)) != NULL) { + while (1) { + PyObject *key; + int status = PyIter_NextItem(it, &key); + if (status < 0) { + goto Fail; + } + if (status == 0) { + break; + } + // setitem_take2_lock_held consumes a reference to key status = setitem_take2_lock_held((PyDictObject *)d, key, Py_NewRef(value)); if (status < 0) { - assert(PyErr_Occurred()); goto Fail; } } } else { - while ((key = PyIter_Next(it)) != NULL) { + while (1) { + PyObject *key; + int status = PyIter_NextItem(it, &key); + if (status < 0) { + goto Fail; + } + if (status == 0) { + break; + } + status = PyObject_SetItem(d, key, value); Py_DECREF(key); - if (status < 0) + if (status < 0) { goto Fail; + } } + } - if (PyErr_Occurred()) - goto Fail; + assert(!PyErr_Occurred()); Py_DECREF(it); goto Done; Fail: + assert(PyErr_Occurred()); Py_XDECREF(it); Py_DECREF(d); return NULL; From 419a70db34dcd2a215d3acec20290f2f64daffe1 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 25 Jun 2026 16:13:47 +0200 Subject: [PATCH 7/9] Document the special frozendict.fromkeys() behavior Call the type constructor with a frozendict for all frozendict subclasses. --- Doc/library/stdtypes.rst | 5 +++++ Lib/test/test_dict.py | 3 ++- Objects/dictobject.c | 7 ++----- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/Doc/library/stdtypes.rst b/Doc/library/stdtypes.rst index a47e1ffb1a6afb..875b265ebbec22 100644 --- a/Doc/library/stdtypes.rst +++ b/Doc/library/stdtypes.rst @@ -5757,6 +5757,11 @@ Frozen dictionaries Like dictionaries, frozendicts are :ref:`generic ` over two types, signifying (respectively) the types of the frozendict's keys and values. + .. classmethod:: fromkeys(iterable, value=None, /) + + Similar to :meth:`dict.fromkeys`, but call the type constructor with a + :class:`frozendict` if the type is a :class:`frozendict` subclass. + .. versionadded:: 3.15 diff --git a/Lib/test/test_dict.py b/Lib/test/test_dict.py index 464292374d8f94..6d61c71e162f8b 100644 --- a/Lib/test/test_dict.py +++ b/Lib/test/test_dict.py @@ -1955,7 +1955,8 @@ def __new__(cls, *args, **kwargs): self.assertEqual(type(fd), FrozenDictSubclass) self.assertEqual(created, frozendict(x=1)) - # Dict subclass which overrides the constructor + # Dict subclass with a constructor which returns a frozendict + # by default class DictSubclass(dict): def __new__(cls, *args, **kwargs): if args or kwargs: diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 2f4d7cb0c0d214..c608089c651b1b 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -3419,12 +3419,9 @@ _PyDict_FromKeys(PyObject *cls, PyObject *iterable, PyObject *value) PyObject *d; int need_copy = 0; - PyTypeObject *cls_type = _PyType_CAST(cls); - if (PyObject_IsSubclass(cls, (PyObject*)&PyFrozenDict_Type) - && cls_type->tp_new == frozendict_new) - { + if (cls == (PyObject*)&PyFrozenDict_Type) { // gh-151722: Create a frozendict which is not tracked by the GC. - d = frozendict_new_untracked(cls_type); + d = frozendict_new_untracked(&PyFrozenDict_Type); } else { // Dict subclass, or frozendict subclass which overrides From 897d768cb1178d1e5b96924e3cbaba4124060331 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 25 Jun 2026 16:16:38 +0200 Subject: [PATCH 8/9] Complete the doc --- Doc/library/stdtypes.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Doc/library/stdtypes.rst b/Doc/library/stdtypes.rst index 875b265ebbec22..886648e820f071 100644 --- a/Doc/library/stdtypes.rst +++ b/Doc/library/stdtypes.rst @@ -5759,8 +5759,10 @@ Frozen dictionaries .. classmethod:: fromkeys(iterable, value=None, /) - Similar to :meth:`dict.fromkeys`, but call the type constructor with a - :class:`frozendict` if the type is a :class:`frozendict` subclass. + Similar to :meth:`dict.fromkeys`, but call again the type constructor + with an initialized :class:`frozendict` if the type is a + :class:`frozendict` subclass or if the constructor returned a + :class:`frozendict`. .. versionadded:: 3.15 From 02d245fd657e5fcca94621054309a15f246aa920 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 25 Jun 2026 18:13:39 +0200 Subject: [PATCH 9/9] Update Objects/dictobject.c Co-authored-by: Inada Naoki --- Objects/dictobject.c | 1 - 1 file changed, 1 deletion(-) diff --git a/Objects/dictobject.c b/Objects/dictobject.c index c608089c651b1b..e34e1b023f4600 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -4530,7 +4530,6 @@ copy_lock_held(PyObject *o, int as_frozendict) if (dict_merge(copy, o, 1, NULL) < 0) { Py_DECREF(copy); return NULL; - } if (PyDict_Check(copy)) {