From bb2c546a21022b6bf09410211094ffd006f760cc Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 18 Feb 2026 11:27:46 +0100 Subject: [PATCH 1/2] gh-141510: Add frozendict_check_mutable() frozendict.fromkeys() now checks if it can mutate the newly created frozendict. --- Lib/test/test_dict.py | 18 ++++++++++++++++++ Objects/dictobject.c | 24 ++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/Lib/test/test_dict.py b/Lib/test/test_dict.py index 21f8bb11071c90..5c0916d5802be0 100644 --- a/Lib/test/test_dict.py +++ b/Lib/test/test_dict.py @@ -1787,6 +1787,24 @@ def test_hash(self): with self.assertRaisesRegex(TypeError, "unhashable type: 'list'"): hash(fd) + def test_fromkeys(self): + self.assertEqual(frozendict.fromkeys('abc'), + frozendict(a=None, b=None, c=None)) + + # special class which keeps a reference to the created dictionary + fd = None + class SpecialDict(frozendict): + def __new__(self): + nonlocal fd + fd = frozendict() + return fd + + errmsg = "cannot mutate frozendict already exposed in Python" + with self.assertRaisesRegex(RuntimeError, errmsg): + SpecialDict.fromkeys(frozendict(x=1)) + with self.assertRaisesRegex(RuntimeError, errmsg): + SpecialDict.fromkeys("def") + if __name__ == "__main__": unittest.main() diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 0959e2c78a3289..80a81a1226bed6 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -138,6 +138,7 @@ As a consequence of this, split keys have a maximum size of 16. // Forward declarations static PyObject* frozendict_new(PyTypeObject *type, PyObject *args, PyObject *kwds); +static int frozendict_check_mutable(PyObject *self); /*[clinic input] @@ -3317,6 +3318,11 @@ _PyDict_FromKeys(PyObject *cls, PyObject *iterable, PyObject *value) } } else if (PyFrozenDict_CheckExact(d)) { + // Check if the class constructor kept a reference to the frozendict + if (frozendict_check_mutable(d) < 0) { + return NULL; + } + if (PyDict_CheckExact(iterable)) { PyDictObject *mp = (PyDictObject *)d; @@ -3360,6 +3366,11 @@ dict_iter_exit:; Py_END_CRITICAL_SECTION(); } else if (PyFrozenDict_CheckExact(d)) { + // Check if the class constructor kept a reference to the frozendict + if (frozendict_check_mutable(d) < 0) { + goto Fail; + } + while ((key = PyIter_Next(it)) != NULL) { // anydict_setitem_take2 consumes a reference to key status = anydict_setitem_take2((PyDictObject *)d, @@ -7889,6 +7900,17 @@ _PyObject_InlineValuesConsistencyCheck(PyObject *obj) // --- frozendict implementation --------------------------------------------- +static int +frozendict_check_mutable(PyObject *self) +{ + if (Py_REFCNT(self) > 1) { + PyErr_SetString(PyExc_RuntimeError, + "cannot mutate frozendict already exposed in Python"); + return -1; + } + return 0; +} + static PyNumberMethods frozendict_as_number = { .nb_or = frozendict_or, }; @@ -7994,6 +8016,8 @@ frozendict_new(PyTypeObject *type, PyObject *args, PyObject *kwds) if (d == NULL) { return NULL; } + assert(Py_REFCNT(self) == 1); + PyFrozenDictObject *self = _PyFrozenDictObject_CAST(d); self->ma_hash = -1; From 5ecf0f951667024fad98b75b0a94a651a3f55818 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 18 Feb 2026 11:37:58 +0100 Subject: [PATCH 2/2] Fix typo --- Objects/dictobject.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 80a81a1226bed6..053d5a465329ee 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -8016,7 +8016,7 @@ frozendict_new(PyTypeObject *type, PyObject *args, PyObject *kwds) if (d == NULL) { return NULL; } - assert(Py_REFCNT(self) == 1); + assert(Py_REFCNT(d) == 1); PyFrozenDictObject *self = _PyFrozenDictObject_CAST(d); self->ma_hash = -1;