From 07c52789e0e0df2f9e432f8897f6f3f87542041d Mon Sep 17 00:00:00 2001 From: tonghuaroot Date: Fri, 7 Aug 2026 18:20:59 +0800 Subject: [PATCH] gh-155324: Pickle frozendict with all protocols frozendict only defined __getnewargs__, so pickling raised TypeError at protocols 0 and 1. Give it a __reduce__ mirroring frozenset. --- Lib/test/test_dict.py | 21 +++++++--------- ...-08-07-18-19-51.gh-issue-155324.Pk4Rz7.rst | 3 +++ Objects/dictobject.c | 24 +++++++++++++++---- 3 files changed, 31 insertions(+), 17 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-08-07-18-19-51.gh-issue-155324.Pk4Rz7.rst diff --git a/Lib/test/test_dict.py b/Lib/test/test_dict.py index dc31d403b837ad..78342f809c11be 100644 --- a/Lib/test/test_dict.py +++ b/Lib/test/test_dict.py @@ -1991,19 +1991,14 @@ def test_pickle(self): if type(fd) == FrozenDictSlots: fd.slot_attr = 456 with self.subTest(fd=fd, proto=proto): - if proto >= 2: - p = pickle.dumps(fd, proto) - fd2 = pickle.loads(p) - self.assertEqual(fd2, fd) - self.assertEqual(type(fd2), type(fd)) - if type(fd) == FrozenDict: - self.assertEqual(fd2.attr, 123) - if type(fd) == FrozenDictSlots: - self.assertEqual(fd2.slot_attr, 456) - else: - # protocol 0 and 1 don't support frozendict - with self.assertRaises(TypeError): - pickle.dumps(fd, proto) + p = pickle.dumps(fd, proto) + fd2 = pickle.loads(p) + self.assertEqual(fd2, fd) + self.assertEqual(type(fd2), type(fd)) + if type(fd) == FrozenDict: + self.assertEqual(fd2.attr, 123) + if type(fd) == FrozenDictSlots: + self.assertEqual(fd2.slot_attr, 456) def test_pickle_iter(self): fd = frozendict(c=1, b=2, a=3, d=4, e=5, f=6) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-08-07-18-19-51.gh-issue-155324.Pk4Rz7.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-07-18-19-51.gh-issue-155324.Pk4Rz7.rst new file mode 100644 index 00000000000000..3eba2262dbe733 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-07-18-19-51.gh-issue-155324.Pk4Rz7.rst @@ -0,0 +1,3 @@ +``frozendict`` can now be pickled with all pickle protocols, matching +:class:`frozenset`. Previously protocols 0 and 1 raised :exc:`TypeError`. +Patch by tonghuaroot. diff --git a/Objects/dictobject.c b/Objects/dictobject.c index c650aa456d2cc9..f7caa48e66d993 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -8344,14 +8344,29 @@ _PyObject_InlineValuesConsistencyCheck(PyObject *obj) // --- frozendict implementation --------------------------------------------- static PyObject * -frozendict_getnewargs(PyObject *op, PyObject *Py_UNUSED(dummy)) +frozendict_reduce(PyObject *op, PyObject *Py_UNUSED(dummy)) { - // Call dict(op): convert 'op' frozendict to a dict + // Mirror frozenset.__reduce__ so frozendict pickles at every protocol + // (__getnewargs__ alone only works at protocol 2 and above). + // Call dict(op) to convert the frozendict to a dict argument. PyObject *arg = PyObject_CallOneArg((PyObject*)&PyDict_Type, op); if (arg == NULL) { return NULL; } - return Py_BuildValue("(N)", arg); + PyObject *args = PyTuple_Pack(1, arg); + Py_DECREF(arg); + if (args == NULL) { + return NULL; + } + PyObject *state = _PyObject_GetState(op); + if (state == NULL) { + Py_DECREF(args); + return NULL; + } + PyObject *result = PyTuple_Pack(3, Py_TYPE(op), args, state); + Py_DECREF(args); + Py_DECREF(state); + return result; } @@ -8377,7 +8392,8 @@ static PyMethodDef frozendict_methods[] = { DICT___REVERSED___METHODDEF {"__class_getitem__", Py_GenericAlias, METH_O|METH_CLASS, PyDoc_STR("frozendicts are generic over two types, signifying (respectively) the types of the frozendict's keys and values")}, - {"__getnewargs__", frozendict_getnewargs, METH_NOARGS}, + {"__reduce__", frozendict_reduce, METH_NOARGS, + PyDoc_STR("Return state information for pickling.")}, {NULL, NULL} /* sentinel */ };