From 99d7ca645c5610f347101f8163264206ae16f37d Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 6 Mar 2026 16:39:15 +0100 Subject: [PATCH 1/4] gh-141510: Optimize frozendict(frozendict) Return the same object unmodified if it's exactly the frozendict type. --- Lib/test/test_dict.py | 7 +++++++ Objects/dictobject.c | 48 +++++++++++++++++++++++++++++++++++-------- 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/Lib/test/test_dict.py b/Lib/test/test_dict.py index 45448d1264a53e..b2f4363b23e748 100644 --- a/Lib/test/test_dict.py +++ b/Lib/test/test_dict.py @@ -1829,6 +1829,13 @@ def test_constructor(self): with self.assertRaises(TypeError): dict.__init__(d, x=1) + # Avoid copy if it's frozendict type + d2 = frozendict(d) + self.assertIs(d2, d) + d2 = FrozenDict(d) + self.assertIsNot(d2, d) + self.assertEqual(d2, d) + def test_copy(self): d = frozendict(x=1, y=2) d2 = d.copy() diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 61fde37f8d4fff..f99a6a1ee85d44 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -5169,15 +5169,47 @@ dict_vectorcall(PyObject *type, PyObject * const*args, return NULL; } - PyObject *self; - if (Py_Is((PyTypeObject*)type, &PyFrozenDict_Type) - || PyType_IsSubtype((PyTypeObject*)type, &PyFrozenDict_Type)) - { - self = frozendict_new(_PyType_CAST(type), NULL, NULL); + PyObject *self = dict_new(_PyType_CAST(type), NULL, NULL); + if (self == NULL) { + return NULL; } - else { - self = dict_new(_PyType_CAST(type), NULL, NULL); + if (nargs == 1) { + if (dict_update_arg(self, args[0]) < 0) { + Py_DECREF(self); + return NULL; + } + args++; } + if (kwnames != NULL) { + for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(kwnames); i++) { + PyObject *key = PyTuple_GET_ITEM(kwnames, i); // borrowed + if (PyDict_SetItem(self, key, args[i]) < 0) { + Py_DECREF(self); + return NULL; + } + } + } + return self; +} + +static PyObject * +frozendict_vectorcall(PyObject *type, PyObject * const*args, + size_t nargsf, PyObject *kwnames) +{ + Py_ssize_t nargs = PyVectorcall_NARGS(nargsf); + if (!_PyArg_CheckPositional("frozendict", nargs, 0, 1)) { + return NULL; + } + + if (nargs == 1 && kwnames == NULL + && PyFrozenDict_CheckExact(args[0]) + && Py_Is((PyTypeObject*)type, &PyFrozenDict_Type)) + { + // frozendict(frozendict) returns the same object unmodified + return Py_NewRef(args[0]); + } + + PyObject *self = frozendict_new(_PyType_CAST(type), NULL, NULL); if (self == NULL) { return NULL; } @@ -8228,6 +8260,6 @@ PyTypeObject PyFrozenDict_Type = { .tp_alloc = _PyType_AllocNoTrack, .tp_new = frozendict_new, .tp_free = PyObject_GC_Del, - .tp_vectorcall = dict_vectorcall, + .tp_vectorcall = frozendict_vectorcall, .tp_version_tag = _Py_TYPE_VERSION_FROZENDICT, }; From 5b07811f825885c56f36680714813c4706082737 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 6 Mar 2026 16:57:54 +0100 Subject: [PATCH 2/4] Optimize also PyFrozenDict_New(frozendict) --- Lib/test/test_capi/test_dict.py | 10 ++++++++++ Objects/dictobject.c | 5 +++++ 2 files changed, 15 insertions(+) diff --git a/Lib/test/test_capi/test_dict.py b/Lib/test/test_capi/test_dict.py index 5bdf74ef73ab54..cd46fea5476ca6 100644 --- a/Lib/test/test_capi/test_dict.py +++ b/Lib/test/test_capi/test_dict.py @@ -619,6 +619,16 @@ def test_frozendict_new(self): self.assertEqual(dct, frozendict(x=1, y=2)) self.assertIs(type(dct), frozendict) + # PyFrozenDict_New(frozendict) returns the same object unmodified + fd = frozendict(a=1, b=2, c=3) + fd2 = frozendict_new(fd) + self.assertIs(fd2, fd) + + fd = FrozenDictSubclass(a=1, b=2, c=3) + fd2 = frozendict_new(fd) + self.assertIsNot(fd2, fd) + self.assertEqual(fd2, fd) + # PyFrozenDict_New(NULL) creates an empty dictionary dct = frozendict_new(NULL) self.assertEqual(dct, frozendict()) diff --git a/Objects/dictobject.c b/Objects/dictobject.c index f99a6a1ee85d44..b5f2a682c54982 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -8203,6 +8203,11 @@ PyObject* PyFrozenDict_New(PyObject *iterable) { if (iterable != NULL) { + if (PyFrozenDict_CheckExact(iterable)) { + // PyFrozenDict_New(frozendict) returns the same object unmodified + return Py_NewRef(iterable); + } + PyObject *args = PyTuple_Pack(1, iterable); if (args == NULL) { return NULL; From 114ffb5bfc1328675cedee1723297bc408eae9cf Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 6 Mar 2026 17:34:27 +0100 Subject: [PATCH 3/4] Document PyFrozenSet_New() optimization --- Doc/c-api/dict.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Doc/c-api/dict.rst b/Doc/c-api/dict.rst index 371761573e97de..8d91ebf444a1e0 100644 --- a/Doc/c-api/dict.rst +++ b/Doc/c-api/dict.rst @@ -542,6 +542,10 @@ Frozen dictionary objects Create an empty dictionary if *iterable* is ``NULL``. + .. impl-detail:: + If *iterable* is a :class:`frozendict`, but not a :class:`frozendict` + subclass, return the same object unmodified. + Ordered dictionaries ^^^^^^^^^^^^^^^^^^^^ From 9de1f08b9c3a45a05fb270c08cd0b89a3ac91bae Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 6 Mar 2026 22:37:16 +0100 Subject: [PATCH 4/4] Simplify the doc; omit subclass special case --- Doc/c-api/dict.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/c-api/dict.rst b/Doc/c-api/dict.rst index 8d91ebf444a1e0..e5f6d44ed71181 100644 --- a/Doc/c-api/dict.rst +++ b/Doc/c-api/dict.rst @@ -543,8 +543,8 @@ Frozen dictionary objects Create an empty dictionary if *iterable* is ``NULL``. .. impl-detail:: - If *iterable* is a :class:`frozendict`, but not a :class:`frozendict` - subclass, return the same object unmodified. + If *iterable* is a :class:`frozendict`, return the same object + unmodified. Ordered dictionaries