From 5ba97b6daff032e37c934c1e7cb431c95c70eae0 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 5 Mar 2026 12:22:43 +0100 Subject: [PATCH 1/3] gh-141510: Add PyFrozenDict_AsDict() function * PyDict_Copy() no longer accepts frozendict. * Remove _PyDict_CopyAsDict() function. * Fix frozendict.items() ^ frozendict.items(). Add non-regression test. --- Doc/c-api/dict.rst | 8 ++--- Include/cpython/dictobject.h | 3 ++ Include/internal/pycore_dict.h | 2 -- Lib/test/test_capi/test_dict.py | 29 ++++++++------- Lib/test/test_dict.py | 13 +++++++ Modules/_testcapi/dict.c | 9 +++++ Objects/clinic/dictobject.c.h | 20 ++++++++++- Objects/dictobject.c | 63 ++++++++++++++++++++++----------- Objects/typeobject.c | 8 ++++- 9 files changed, 115 insertions(+), 40 deletions(-) diff --git a/Doc/c-api/dict.rst b/Doc/c-api/dict.rst index 734462bc0051af..06e0ff103509b5 100644 --- a/Doc/c-api/dict.rst +++ b/Doc/c-api/dict.rst @@ -82,10 +82,6 @@ Dictionary objects Return a new dictionary that contains the same key-value pairs as *p*. - .. versionchanged:: next - If *p* is a subclass of :class:`frozendict`, the result will be a - :class:`frozendict` instance instead of a :class:`dict` instance. - .. c:function:: int PyDict_SetItem(PyObject *p, PyObject *key, PyObject *val) Insert *val* into the dictionary *p* with a key of *key*. *key* must be @@ -546,6 +542,10 @@ Frozen dictionary objects Create an empty dictionary if *iterable* is ``NULL``. +.. c:function:: PyObject* PyFrozenDict_AsDict(PyObject *p) + + Convert a :class:`frozendict` to a :class:`dict` (create a copy). + Ordered dictionaries ^^^^^^^^^^^^^^^^^^^^ diff --git a/Include/cpython/dictobject.h b/Include/cpython/dictobject.h index 5e7811416aba63..f9c71178208dbc 100644 --- a/Include/cpython/dictobject.h +++ b/Include/cpython/dictobject.h @@ -106,3 +106,6 @@ PyAPI_FUNC(int) PyDict_Unwatch(int watcher_id, PyObject* dict); // Create a frozendict. Create an empty dictionary if iterable is NULL. PyAPI_FUNC(PyObject*) PyFrozenDict_New(PyObject *iterable); + +// Convert a frozendict to a dict (create a copy). +PyAPI_FUNC(PyObject*) PyFrozenDict_AsDict(PyObject *o); diff --git a/Include/internal/pycore_dict.h b/Include/internal/pycore_dict.h index 1aeec32f55a7f3..59e88be6aeec12 100644 --- a/Include/internal/pycore_dict.h +++ b/Include/internal/pycore_dict.h @@ -160,8 +160,6 @@ extern void _PyDict_Clear_LockHeld(PyObject *op); PyAPI_FUNC(void) _PyDict_EnsureSharedOnRead(PyDictObject *mp); #endif -extern PyObject* _PyDict_CopyAsDict(PyObject *op); - #define DKIX_EMPTY (-1) #define DKIX_DUMMY (-2) /* Used internally */ #define DKIX_ERROR (-3) diff --git a/Lib/test/test_capi/test_dict.py b/Lib/test/test_capi/test_dict.py index 561e1ea4d52846..51050647641bed 100644 --- a/Lib/test/test_capi/test_dict.py +++ b/Lib/test/test_capi/test_dict.py @@ -97,21 +97,13 @@ def test_dictproxy_new(self): def test_dict_copy(self): # Test PyDict_Copy() copy = _testlimitedcapi.dict_copy - for dict_type in ANYDICT_TYPES: + for dict_type in DICT_TYPES: dct = dict_type({1: 2}) dct_copy = copy(dct) - if dict_type == frozendict: - expected_type = frozendict - self.assertIs(dct_copy, dct) - else: - if issubclass(dict_type, frozendict): - expected_type = frozendict - else: - expected_type = dict - self.assertIs(type(dct_copy), expected_type) - self.assertEqual(dct_copy, dct) + self.assertIs(type(dct_copy), dict) + self.assertEqual(dct_copy, dct) - for test_type in NOT_ANYDICT_TYPES + OTHER_TYPES: + for test_type in NOT_DICT_TYPES + OTHER_TYPES: self.assertRaises(SystemError, copy, test_type()) self.assertRaises(SystemError, copy, NULL) @@ -632,6 +624,19 @@ def test_frozendict_new(self): self.assertEqual(dct, frozendict()) self.assertIs(type(dct), frozendict) + def test_frozendict_asdict(self): + # Test PyFrozenDict_AsDict() + frozendict_asdict = _testlimitedcapi.frozendict_asdict + for dict_type in FROZENDICT_TYPES: + dct = dict_type({1: 2}) + dct_copy = frozendict_asdict(dct) + self.assertIs(type(dct_copy), dict) + self.assertEqual(dct_copy, dct) + + for test_type in NOT_FROZENDICT_TYPES + OTHER_TYPES: + self.assertRaises(SystemError, frozendict_asdict, test_type()) + self.assertRaises(SystemError, frozendict_asdict, NULL) + if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_dict.py b/Lib/test/test_dict.py index 162b0b38f8555d..45448d1264a53e 100644 --- a/Lib/test/test_dict.py +++ b/Lib/test/test_dict.py @@ -1848,11 +1848,19 @@ def test_merge(self): frozendict({'x': 1, 'y': 2})) self.assertEqual(frozendict(x=1, y=2) | frozendict(y=5), frozendict({'x': 1, 'y': 5})) + self.assertEqual(FrozenDict(x=1, y=2) | FrozenDict(y=5), + frozendict({'x': 1, 'y': 5})) + fd = frozendict(x=1, y=2) self.assertIs(fd | frozendict(), fd) self.assertIs(fd | {}, fd) self.assertIs(frozendict() | fd, fd) + fd = FrozenDict(x=1, y=2) + self.assertEqual(fd | frozendict(), fd) + self.assertEqual(fd | {}, fd) + self.assertEqual(frozendict() | fd, fd) + def test_update(self): # test "a |= b" operator d = frozendict(x=1) @@ -1863,6 +1871,11 @@ def test_update(self): self.assertEqual(d, frozendict({'x': 1, 'y': 2})) self.assertEqual(copy, frozendict({'x': 1})) + def test_items_xor(self): + # test "a ^ b" operator on items views + res = frozendict(a=1, b=2).items() ^ frozendict(b=2, c=3).items() + self.assertEqual(res, {('a', 1), ('c', 3)}) + def test_repr(self): d = frozendict() self.assertEqual(repr(d), "frozendict()") diff --git a/Modules/_testcapi/dict.c b/Modules/_testcapi/dict.c index 172591b03182ab..724b5b37bbd52b 100644 --- a/Modules/_testcapi/dict.c +++ b/Modules/_testcapi/dict.c @@ -295,6 +295,14 @@ frozendict_new(PyObject *self, PyObject *obj) } +static PyObject* +frozendict_asdict(PyObject *self, PyObject *obj) +{ + NULLABLE(obj); + return PyFrozenDict_AsDict(obj); +} + + static PyMethodDef test_methods[] = { {"dict_containsstring", dict_containsstring, METH_VARARGS}, {"dict_getitemref", dict_getitemref, METH_VARARGS}, @@ -311,6 +319,7 @@ static PyMethodDef test_methods[] = { {"anydict_check", anydict_check, METH_O}, {"anydict_checkexact", anydict_checkexact, METH_O}, {"frozendict_new", frozendict_new, METH_O}, + {"frozendict_asdict", frozendict_asdict, METH_O}, {NULL}, }; diff --git a/Objects/clinic/dictobject.c.h b/Objects/clinic/dictobject.c.h index abf6b38449fcb0..15b8705d9c78e3 100644 --- a/Objects/clinic/dictobject.c.h +++ b/Objects/clinic/dictobject.c.h @@ -323,4 +323,22 @@ dict_values(PyObject *self, PyObject *Py_UNUSED(ignored)) { return dict_values_impl((PyDictObject *)self); } -/*[clinic end generated code: output=9007b74432217017 input=a9049054013a1b77]*/ + +PyDoc_STRVAR(frozendict_copy__doc__, +"copy($self, /)\n" +"--\n" +"\n" +"Return a shallow copy of the frozendict."); + +#define FROZENDICT_COPY_METHODDEF \ + {"copy", (PyCFunction)frozendict_copy, METH_NOARGS, frozendict_copy__doc__}, + +static PyObject * +frozendict_copy_impl(PyFrozenDictObject *self); + +static PyObject * +frozendict_copy(PyObject *self, PyObject *Py_UNUSED(ignored)) +{ + return frozendict_copy_impl((PyFrozenDictObject *)self); +} +/*[clinic end generated code: output=f4c88a3464928ae3 input=a9049054013a1b77]*/ diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 2552216152f98d..8229526796314a 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -146,8 +146,9 @@ static int dict_merge_from_seq2(PyObject *d, PyObject *seq2, int override); /*[clinic input] class dict "PyDictObject *" "&PyDict_Type" +class frozendict "PyFrozenDictObject *" "&PyFrozenDict_Type" [clinic start generated code]*/ -/*[clinic end generated code: output=da39a3ee5e6b4b0d input=f157a5a0ce9589d6]*/ +/*[clinic end generated code: output=da39a3ee5e6b4b0d input=5dfa93bac68e7c54]*/ /* @@ -4384,7 +4385,23 @@ copy_lock_held(PyObject *o, int as_frozendict) return NULL; } -// Similar to PyDict_Copy(), but copy also frozendict. +PyObject * +PyDict_Copy(PyObject *o) +{ + if (o == NULL || !PyDict_Check(o)) { + PyErr_BadInternalCall(); + return NULL; + } + + PyObject *res; + Py_BEGIN_CRITICAL_SECTION(o); + res = copy_lock_held(o, 0); + Py_END_CRITICAL_SECTION(); + return res; +} + +// Similar to PyDict_Copy(), but return a frozendict if the argument +// is a frozendict. static PyObject * _PyDict_Copy(PyObject *o) { @@ -4397,27 +4414,14 @@ _PyDict_Copy(PyObject *o) return res; } -PyObject * -PyDict_Copy(PyObject *o) +PyObject* +PyFrozenDict_AsDict(PyObject *o) { - if (o == NULL || !PyAnyDict_Check(o)) { + if (o == NULL || !PyFrozenDict_Check(o)) { PyErr_BadInternalCall(); return NULL; } - if (PyFrozenDict_CheckExact(o)) { - return Py_NewRef(o); - } - - return _PyDict_Copy(o); -} - -// Similar to PyDict_Copy(), but return a dict if the argument is a frozendict. -PyObject* -_PyDict_CopyAsDict(PyObject *o) -{ - assert(PyAnyDict_Check(o)); - PyObject *res; Py_BEGIN_CRITICAL_SECTION(o); res = copy_lock_held(o, 0); @@ -6523,7 +6527,7 @@ dictitems_xor_lock_held(PyObject *d1, PyObject *d2) ASSERT_DICT_LOCKED(d1); ASSERT_DICT_LOCKED(d2); - PyObject *temp_dict = copy_lock_held(d1, PyFrozenDict_Check(d1)); + PyObject *temp_dict = copy_lock_held(d1, 0); if (temp_dict == NULL) { return NULL; } @@ -8057,7 +8061,7 @@ static PyMethodDef frozendict_methods[] = { DICT_ITEMS_METHODDEF DICT_VALUES_METHODDEF DICT_FROMKEYS_METHODDEF - DICT_COPY_METHODDEF + FROZENDICT_COPY_METHODDEF DICT___REVERSED___METHODDEF {"__class_getitem__", Py_GenericAlias, METH_O|METH_CLASS, PyDoc_STR("See PEP 585")}, {"__getnewargs__", frozendict_getnewargs, METH_NOARGS}, @@ -8182,6 +8186,25 @@ PyFrozenDict_New(PyObject *iterable) } } +/*[clinic input] +frozendict.copy + +Return a shallow copy of the frozendict. +[clinic start generated code]*/ + +static PyObject * +frozendict_copy_impl(PyFrozenDictObject *self) +/*[clinic end generated code: output=e580fd91d9fc2cf7 input=35f6abeaa08fd4bc]*/ +{ + assert(PyFrozenDict_Check(self)); + + if (PyFrozenDict_CheckExact(self)) { + return Py_NewRef(self); + } + + return _PyDict_Copy((PyObject*)self); +} + PyTypeObject PyFrozenDict_Type = { PyVarObject_HEAD_INIT(&PyType_Type, 0) diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 1fdd3cbdaaa639..e88cf09b3147db 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -4850,7 +4850,13 @@ type_new_get_slots(type_new_ctx *ctx, PyObject *dict) static PyTypeObject* type_new_init(type_new_ctx *ctx) { - PyObject *dict = _PyDict_CopyAsDict(ctx->orig_dict); + PyObject *dict; + if (PyFrozenDict_Check(ctx->orig_dict)) { + dict = PyFrozenDict_AsDict(ctx->orig_dict); + } + else { + dict = PyDict_Copy(ctx->orig_dict); + } if (dict == NULL) { goto error; } From 741f01335c6e0744baac7f1f23b4c7eb75700543 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 5 Mar 2026 12:49:06 +0100 Subject: [PATCH 2/3] Fix typo in C API tests --- Lib/test/test_capi/test_dict.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_capi/test_dict.py b/Lib/test/test_capi/test_dict.py index 51050647641bed..b9b990eb4a172a 100644 --- a/Lib/test/test_capi/test_dict.py +++ b/Lib/test/test_capi/test_dict.py @@ -626,7 +626,7 @@ def test_frozendict_new(self): def test_frozendict_asdict(self): # Test PyFrozenDict_AsDict() - frozendict_asdict = _testlimitedcapi.frozendict_asdict + frozendict_asdict = _testcapi.frozendict_asdict for dict_type in FROZENDICT_TYPES: dct = dict_type({1: 2}) dct_copy = frozendict_asdict(dct) From 568ca96e14189ac96809bcb9977226512cf50873 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 5 Mar 2026 16:39:11 +0100 Subject: [PATCH 3/3] Replace the function with PyAnyDict_AsNewDict() --- Doc/c-api/dict.rst | 4 ++-- Include/cpython/dictobject.h | 4 ++-- Lib/test/test_capi/test_dict.py | 16 ++++++++-------- Modules/_elementtree.c | 14 ++------------ Modules/_testcapi/dict.c | 6 +++--- Objects/dictobject.c | 4 ++-- Objects/typeobject.c | 8 +------- 7 files changed, 20 insertions(+), 36 deletions(-) diff --git a/Doc/c-api/dict.rst b/Doc/c-api/dict.rst index 06e0ff103509b5..43f08af322d4a3 100644 --- a/Doc/c-api/dict.rst +++ b/Doc/c-api/dict.rst @@ -542,9 +542,9 @@ Frozen dictionary objects Create an empty dictionary if *iterable* is ``NULL``. -.. c:function:: PyObject* PyFrozenDict_AsDict(PyObject *p) +.. c:function:: PyObject* PyAnyDict_AsNewDict(PyObject *p) - Convert a :class:`frozendict` to a :class:`dict` (create a copy). + Create a new dictionary from a :class:`dict` or a :class:`frozendict`. Ordered dictionaries diff --git a/Include/cpython/dictobject.h b/Include/cpython/dictobject.h index f9c71178208dbc..fe84fa149d5233 100644 --- a/Include/cpython/dictobject.h +++ b/Include/cpython/dictobject.h @@ -107,5 +107,5 @@ PyAPI_FUNC(int) PyDict_Unwatch(int watcher_id, PyObject* dict); // Create a frozendict. Create an empty dictionary if iterable is NULL. PyAPI_FUNC(PyObject*) PyFrozenDict_New(PyObject *iterable); -// Convert a frozendict to a dict (create a copy). -PyAPI_FUNC(PyObject*) PyFrozenDict_AsDict(PyObject *o); +// Create a new dictionary from a dict or a frozendict. +PyAPI_FUNC(PyObject*) PyAnyDict_AsNewDict(PyObject *o); diff --git a/Lib/test/test_capi/test_dict.py b/Lib/test/test_capi/test_dict.py index b9b990eb4a172a..80c038db352203 100644 --- a/Lib/test/test_capi/test_dict.py +++ b/Lib/test/test_capi/test_dict.py @@ -624,18 +624,18 @@ def test_frozendict_new(self): self.assertEqual(dct, frozendict()) self.assertIs(type(dct), frozendict) - def test_frozendict_asdict(self): - # Test PyFrozenDict_AsDict() - frozendict_asdict = _testcapi.frozendict_asdict - for dict_type in FROZENDICT_TYPES: + def test_anydict_asnewdict(self): + # Test PyAnyDict_AsNewDict() + anydict_asnewdict = _testcapi.anydict_asnewdict + for dict_type in ANYDICT_TYPES: dct = dict_type({1: 2}) - dct_copy = frozendict_asdict(dct) + dct_copy = anydict_asnewdict(dct) self.assertIs(type(dct_copy), dict) self.assertEqual(dct_copy, dct) - for test_type in NOT_FROZENDICT_TYPES + OTHER_TYPES: - self.assertRaises(SystemError, frozendict_asdict, test_type()) - self.assertRaises(SystemError, frozendict_asdict, NULL) + for test_type in NOT_ANYDICT_TYPES + OTHER_TYPES: + self.assertRaises(SystemError, anydict_asnewdict, test_type()) + self.assertRaises(SystemError, anydict_asnewdict, NULL) if __name__ == "__main__": diff --git a/Modules/_elementtree.c b/Modules/_elementtree.c index 0f762d411a3163..00cc1acb22f15f 100644 --- a/Modules/_elementtree.c +++ b/Modules/_elementtree.c @@ -389,12 +389,7 @@ get_attrib_from_keywords(PyObject *kwds) Py_DECREF(attrib); return NULL; } - if (PyFrozenDict_Check(attrib)) { - Py_SETREF(attrib, PyFrozenDict_AsDict(attrib)); - } - else { - Py_SETREF(attrib, PyDict_Copy(attrib)); - } + Py_SETREF(attrib, PyAnyDict_AsNewDict(attrib)); } else { attrib = PyDict_New(); @@ -433,12 +428,7 @@ element_init(PyObject *self, PyObject *args, PyObject *kwds) if (attrib) { /* attrib passed as positional arg */ - if (PyFrozenDict_Check(attrib)) { - attrib = PyFrozenDict_AsDict(attrib); - } - else { - attrib = PyDict_Copy(attrib); - } + attrib = PyAnyDict_AsNewDict(attrib); if (!attrib) return -1; if (kwds) { diff --git a/Modules/_testcapi/dict.c b/Modules/_testcapi/dict.c index 724b5b37bbd52b..491f21299c6613 100644 --- a/Modules/_testcapi/dict.c +++ b/Modules/_testcapi/dict.c @@ -296,10 +296,10 @@ frozendict_new(PyObject *self, PyObject *obj) static PyObject* -frozendict_asdict(PyObject *self, PyObject *obj) +anydict_asnewdict(PyObject *self, PyObject *obj) { NULLABLE(obj); - return PyFrozenDict_AsDict(obj); + return PyAnyDict_AsNewDict(obj); } @@ -319,7 +319,7 @@ static PyMethodDef test_methods[] = { {"anydict_check", anydict_check, METH_O}, {"anydict_checkexact", anydict_checkexact, METH_O}, {"frozendict_new", frozendict_new, METH_O}, - {"frozendict_asdict", frozendict_asdict, METH_O}, + {"anydict_asnewdict", anydict_asnewdict, METH_O}, {NULL}, }; diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 77935e297f0b16..32f6b68f7fc01f 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -4415,9 +4415,9 @@ anydict_copy(PyObject *o) } PyObject* -PyFrozenDict_AsDict(PyObject *o) +PyAnyDict_AsNewDict(PyObject *o) { - if (o == NULL || !PyFrozenDict_Check(o)) { + if (o == NULL || !PyAnyDict_Check(o)) { PyErr_BadInternalCall(); return NULL; } diff --git a/Objects/typeobject.c b/Objects/typeobject.c index e88cf09b3147db..9ceaba5efc8e8d 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -4850,13 +4850,7 @@ type_new_get_slots(type_new_ctx *ctx, PyObject *dict) static PyTypeObject* type_new_init(type_new_ctx *ctx) { - PyObject *dict; - if (PyFrozenDict_Check(ctx->orig_dict)) { - dict = PyFrozenDict_AsDict(ctx->orig_dict); - } - else { - dict = PyDict_Copy(ctx->orig_dict); - } + PyObject *dict = PyAnyDict_AsNewDict(ctx->orig_dict); if (dict == NULL) { goto error; }