From 3eb714a09337521ff88bf53ef939452a536b4fca Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 18 Feb 2026 19:14:44 +0100 Subject: [PATCH 01/10] gh-141510, PEP 814: Add frozendict support to pickle Add frozendict.__getnewargs__() method. --- Lib/test/picklecommon.py | 5 ++++- Lib/test/pickletester.py | 7 ++++++- Objects/dictobject.c | 13 +++++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/Lib/test/picklecommon.py b/Lib/test/picklecommon.py index b749ee09f564bf..dd55d8fe19a1a2 100644 --- a/Lib/test/picklecommon.py +++ b/Lib/test/picklecommon.py @@ -252,6 +252,9 @@ class MyList(list): class MyDict(dict): sample = {"a": 1, "b": 2} +class MyFrozenDict(dict): + sample = frozendict({"a": 1, "b": 2}) + class MySet(set): sample = {"a", "b"} @@ -261,7 +264,7 @@ class MyFrozenSet(frozenset): myclasses = [MyInt, MyLong, MyFloat, MyComplex, MyStr, MyUnicode, - MyTuple, MyList, MyDict, MySet, MyFrozenSet] + MyTuple, MyList, MyDict, MyFrozenDict, MySet, MyFrozenSet] # For test_newobj_overridden_new class MyIntWithNew(int): diff --git a/Lib/test/pickletester.py b/Lib/test/pickletester.py index 3d4ed8a2b6ee40..73741bb0ea8cd4 100644 --- a/Lib/test/pickletester.py +++ b/Lib/test/pickletester.py @@ -2839,11 +2839,13 @@ def test_recursive_multi(self): self.assertEqual(list(x[0].attr.keys()), [1]) self.assertIs(x[0].attr[1], x) - def _test_recursive_collection_and_inst(self, factory, oldminproto=None): + def _test_recursive_collection_and_inst(self, factory, oldminproto=None, + minprotocol=0): if self.py_version < (3, 0): self.skipTest('"classic" classes are not interoperable with Python 2') # Mutable object containing a collection containing the original # object. + protocols = range(minprotocol, pickle.HIGHEST_PROTOCOL + 1) o = Object() o.attr = factory([o]) t = type(o.attr) @@ -2883,6 +2885,9 @@ def test_recursive_tuple_and_inst(self): def test_recursive_dict_and_inst(self): self._test_recursive_collection_and_inst(dict.fromkeys, oldminproto=0) + def test_recursive_frozendict_and_inst(self): + self._test_recursive_collection_and_inst(frozendict.fromkeys, minprotocol=2) + def test_recursive_set_and_inst(self): self._test_recursive_collection_and_inst(set) diff --git a/Objects/dictobject.c b/Objects/dictobject.c index af3fcca7455470..8f960352fa4824 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -7930,6 +7930,18 @@ _PyObject_InlineValuesConsistencyCheck(PyObject *obj) // --- frozendict implementation --------------------------------------------- +static PyObject * +frozendict_getnewargs(PyObject *op, PyObject *Py_UNUSED(dummy)) +{ + // Call dict(op): convert 'op' frozendict to a dict + PyObject *arg = PyObject_CallOneArg((PyObject*)&PyDict_Type, op); + if (arg == NULL) { + return NULL; + } + return Py_BuildValue("(N)", arg); +} + + static PyNumberMethods frozendict_as_number = { .nb_or = frozendict_or, }; @@ -7951,6 +7963,7 @@ static PyMethodDef frozendict_methods[] = { DICT_COPY_METHODDEF DICT___REVERSED___METHODDEF {"__class_getitem__", Py_GenericAlias, METH_O|METH_CLASS, PyDoc_STR("See PEP 585")}, + {"__getnewargs__", frozendict_getnewargs, METH_NOARGS}, {NULL, NULL} /* sentinel */ }; From d7972ffc98282bba0deedcabcf66a182f0abf9c0 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 18 Feb 2026 19:26:49 +0100 Subject: [PATCH 02/10] Add simple tests --- Lib/test/pickletester.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Lib/test/pickletester.py b/Lib/test/pickletester.py index 73741bb0ea8cd4..02104f52fdb390 100644 --- a/Lib/test/pickletester.py +++ b/Lib/test/pickletester.py @@ -3100,6 +3100,15 @@ def test_float_format(self): # make sure that floats are formatted locale independent with proto 0 self.assertEqual(self.dumps(1.2, 0)[0:3], b'F1.') + def test_frozendict(self): + for proto in range(2, pickle.HIGHEST_PROTOCOL + 1): + for fd in ( + frozendict(), + frozendict(x=1, y=2), + ): + p = self.dumps(fd, proto) + self.assert_is_copy(fd, self.loads(p)) + def test_reduce(self): for proto in protocols: with self.subTest(proto=proto): From 24d757ad4bd9178be6ccf76345714f9c22ac17d0 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 19 Feb 2026 13:13:34 +0100 Subject: [PATCH 03/10] Fix picklecommon for Python 3.14 and older --- Lib/test/picklecommon.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Lib/test/picklecommon.py b/Lib/test/picklecommon.py index dd55d8fe19a1a2..bb8e41b01492ea 100644 --- a/Lib/test/picklecommon.py +++ b/Lib/test/picklecommon.py @@ -252,9 +252,6 @@ class MyList(list): class MyDict(dict): sample = {"a": 1, "b": 2} -class MyFrozenDict(dict): - sample = frozendict({"a": 1, "b": 2}) - class MySet(set): sample = {"a", "b"} @@ -264,7 +261,18 @@ class MyFrozenSet(frozenset): myclasses = [MyInt, MyLong, MyFloat, MyComplex, MyStr, MyUnicode, - MyTuple, MyList, MyDict, MyFrozenDict, MySet, MyFrozenSet] + MyTuple, MyList, MyDict, MySet, MyFrozenSet] + +try: + frozendict +except NameError: + # Python 3.14 and older + pass +else: + class MyFrozenDict(dict): + sample = frozendict({"a": 1, "b": 2}) + myclasses.append(MyFrozenDict) + # For test_newobj_overridden_new class MyIntWithNew(int): From 416a1b7650e6505e775c2b31332e1f08c13ed050 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 19 Feb 2026 13:24:53 +0100 Subject: [PATCH 04/10] Move pickle tests to test_dict --- Lib/test/pickletester.py | 11 ++--------- Lib/test/test_dict.py | 9 +++++++++ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/Lib/test/pickletester.py b/Lib/test/pickletester.py index 02104f52fdb390..bf7ebd2bdfb4f2 100644 --- a/Lib/test/pickletester.py +++ b/Lib/test/pickletester.py @@ -2886,6 +2886,8 @@ def test_recursive_dict_and_inst(self): self._test_recursive_collection_and_inst(dict.fromkeys, oldminproto=0) def test_recursive_frozendict_and_inst(self): + if self.py_version < (3, 15): + self.skipTest('need frozendict') self._test_recursive_collection_and_inst(frozendict.fromkeys, minprotocol=2) def test_recursive_set_and_inst(self): @@ -3100,15 +3102,6 @@ def test_float_format(self): # make sure that floats are formatted locale independent with proto 0 self.assertEqual(self.dumps(1.2, 0)[0:3], b'F1.') - def test_frozendict(self): - for proto in range(2, pickle.HIGHEST_PROTOCOL + 1): - for fd in ( - frozendict(), - frozendict(x=1, y=2), - ): - p = self.dumps(fd, proto) - self.assert_is_copy(fd, self.loads(p)) - def test_reduce(self): for proto in protocols: with self.subTest(proto=proto): diff --git a/Lib/test/test_dict.py b/Lib/test/test_dict.py index 71f72cb2557670..4a7447cdec56d6 100644 --- a/Lib/test/test_dict.py +++ b/Lib/test/test_dict.py @@ -1825,6 +1825,15 @@ def __new__(self): self.assertEqual(type(fd), DictSubclass) self.assertEqual(created, frozendict(x=1)) + def test_pickle(self): + for proto in range(2, pickle.HIGHEST_PROTOCOL + 1): + for fd in ( + frozendict(), + frozendict(x=1, y=2), + ): + p = pickle.dumps(fd, proto) + self.assert_is_copy(fd, pickle.loads(p)) + if __name__ == "__main__": unittest.main() From 9666d7e4d99a8db2cf74b57e6657de3c3ebc8697 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 19 Feb 2026 13:37:40 +0100 Subject: [PATCH 05/10] Fix test_dict.test_pickle() --- Lib/test/test_dict.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_dict.py b/Lib/test/test_dict.py index 4a7447cdec56d6..70a649158832d0 100644 --- a/Lib/test/test_dict.py +++ b/Lib/test/test_dict.py @@ -1832,7 +1832,7 @@ def test_pickle(self): frozendict(x=1, y=2), ): p = pickle.dumps(fd, proto) - self.assert_is_copy(fd, pickle.loads(p)) + self.assertEqual(fd, pickle.loads(p)) if __name__ == "__main__": From 2939cba612542a2adc24ca21933f6570fe397171 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 19 Feb 2026 14:18:38 +0100 Subject: [PATCH 06/10] Test frozendict subclasses --- Lib/test/test_dict.py | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/Lib/test/test_dict.py b/Lib/test/test_dict.py index 70a649158832d0..21a2572471af61 100644 --- a/Lib/test/test_dict.py +++ b/Lib/test/test_dict.py @@ -1731,6 +1731,12 @@ class FrozenDict(frozendict): pass +class FrozenDictSlots(frozendict): + __slots__ = ('attr',) + def __init__(self, *args, **kwargs): + self.attr = 123 + + class FrozenDictTests(unittest.TestCase): def test_copy(self): d = frozendict(x=1, y=2) @@ -1773,10 +1779,8 @@ def test_repr(self): d = frozendict(x=1, y=2) self.assertEqual(repr(d), "frozendict({'x': 1, 'y': 2})") - class MyFrozenDict(frozendict): - pass - d = MyFrozenDict(x=1, y=2) - self.assertEqual(repr(d), "MyFrozenDict({'x': 1, 'y': 2})") + d = FrozenDict(x=1, y=2) + self.assertEqual(repr(d), "FrozenDict({'x': 1, 'y': 2})") def test_hash(self): # hash() doesn't rely on the items order @@ -1826,13 +1830,23 @@ def __new__(self): self.assertEqual(created, frozendict(x=1)) def test_pickle(self): - for proto in range(2, pickle.HIGHEST_PROTOCOL + 1): + for proto in range(pickle.HIGHEST_PROTOCOL + 1): for fd in ( frozendict(), frozendict(x=1, y=2), + FrozenDict(x=1, y=2), + FrozenDictSlots(x=1, y=2), ): - p = pickle.dumps(fd, proto) - self.assertEqual(fd, pickle.loads(p)) + 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)) + else: + # protocol 0 and 1 don't support frozendict + with self.assertRaises(TypeError): + pickle.dumps(fd, proto) if __name__ == "__main__": From 8162f6bd9f42d5308c0581aba091adf9969c20d1 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 19 Feb 2026 23:57:13 +0100 Subject: [PATCH 07/10] Add more tests to test_dict --- Lib/test/test_dict.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_dict.py b/Lib/test/test_dict.py index 21a2572471af61..b3abe66b2a6815 100644 --- a/Lib/test/test_dict.py +++ b/Lib/test/test_dict.py @@ -1732,9 +1732,7 @@ class FrozenDict(frozendict): class FrozenDictSlots(frozendict): - __slots__ = ('attr',) - def __init__(self, *args, **kwargs): - self.attr = 123 + __slots__ = ('slot_attr',) class FrozenDictTests(unittest.TestCase): @@ -1837,17 +1835,32 @@ def test_pickle(self): FrozenDict(x=1, y=2), FrozenDictSlots(x=1, y=2), ): + if type(fd) == FrozenDict: + fd.attr = 123 + 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) + def test_pickle_iter(self): + it = iter(frozendict(x=1, y=2)) + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + p = pickle.dumps(it, proto) + it2 = pickle.loads(p) + self.assertEqual(list(it2), ['x', 'y']) + if __name__ == "__main__": unittest.main() From 64cba52c210b8a9dcdf7229e39ed07c1dee4f947 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 20 Feb 2026 00:16:33 +0100 Subject: [PATCH 08/10] Add pickle tests on mutable key/value --- Lib/test/picklecommon.py | 3 +++ Lib/test/pickletester.py | 48 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/Lib/test/picklecommon.py b/Lib/test/picklecommon.py index bb8e41b01492ea..a8c8b92f9fb31d 100644 --- a/Lib/test/picklecommon.py +++ b/Lib/test/picklecommon.py @@ -243,6 +243,9 @@ class MyUnicode(str): class MyUnicode(unicode): sample = unicode(r"hello \u1234", "raw-unicode-escape") +class DictKey: + pass + class MyTuple(tuple): sample = (1, 2, 3) diff --git a/Lib/test/pickletester.py b/Lib/test/pickletester.py index bf7ebd2bdfb4f2..3a1d0592e64155 100644 --- a/Lib/test/pickletester.py +++ b/Lib/test/pickletester.py @@ -2911,6 +2911,54 @@ def test_recursive_set_subclass_and_inst(self): def test_recursive_frozenset_subclass_and_inst(self): self._test_recursive_collection_and_inst(MyFrozenSet) + def _test_recursive_collection_in_key(self, factory, minprotocol=0): + protocols = range(minprotocol, pickle.HIGHEST_PROTOCOL + 1) + key = DictKey() + o = factory({key: 1}) + key.attr = o + for proto in protocols: + with self.subTest(proto=proto): + s = self.dumps(o, proto) + x = self.loads(s) + keys = list(x.keys()) + self.assertEqual(len(keys), 1) + self.assertIs(keys[0].attr, x) + + def test_recursive_dict_in_key(self): + self._test_recursive_collection_in_key(dict) + + def test_recursive_dict_subclass_in_key(self): + self._test_recursive_collection_in_key(MyDict) + + def test_recursive_frozendict_in_key(self): + self._test_recursive_collection_in_key(frozendict, minprotocol=2) + + def test_recursive_frozendict_subclass_in_key(self): + self._test_recursive_collection_in_key(MyFrozenDict) + + def _test_recursive_collection_in_value(self, factory, minprotocol=0): + protocols = range(minprotocol, pickle.HIGHEST_PROTOCOL + 1) + o = factory(key=[]) + o['key'].append(o) + for proto in protocols: + with self.subTest(proto=proto): + s = self.dumps(o, proto) + x = self.loads(s) + self.assertEqual(len(x['key']), 1) + self.assertIs(x['key'][0], x) + + def test_recursive_dict_in_value(self): + self._test_recursive_collection_in_value(dict) + + def test_recursive_dict_subclass_in_value(self): + self._test_recursive_collection_in_value(MyDict) + + def test_recursive_frozendict_in_value(self): + self._test_recursive_collection_in_value(frozendict, minprotocol=2) + + def test_recursive_frozendict_subclass_in_value(self): + self._test_recursive_collection_in_value(MyFrozenDict) + def test_recursive_inst_state(self): # Mutable object containing itself. y = REX_state() From 90842c1063f686addf628282b380d5d2e5463806 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 20 Feb 2026 14:23:42 +0100 Subject: [PATCH 09/10] Add more pickle tests on iterators --- Lib/test/test_dict.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_dict.py b/Lib/test/test_dict.py index b3abe66b2a6815..9cfaa4a86fa9fd 100644 --- a/Lib/test/test_dict.py +++ b/Lib/test/test_dict.py @@ -1855,11 +1855,21 @@ def test_pickle(self): pickle.dumps(fd, proto) def test_pickle_iter(self): - it = iter(frozendict(x=1, y=2)) - for proto in range(pickle.HIGHEST_PROTOCOL + 1): - p = pickle.dumps(it, proto) - it2 = pickle.loads(p) - self.assertEqual(list(it2), ['x', 'y']) + fd = frozendict(c=1, b=2, a=3, d=4, e=5, f=6) + for method_name in (None, 'keys', 'values', 'items'): + if method_name is not None: + meth = getattr(fd, method_name) + else: + meth = lambda: fd + expected = list(meth())[1:] + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + with self.subTest(method_name=method_name, protocol=proto): + it = iter(meth()) + next(it) + p = pickle.dumps(it, proto) + unpickled = pickle.loads(p) + self.assertEqual(list(unpickled), expected) + self.assertEqual(list(it), expected) if __name__ == "__main__": From c5f7ef7798d3c262d1a08ecd145385bfedb98586 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 20 Feb 2026 14:24:59 +0100 Subject: [PATCH 10/10] Remove redundant pickle tests Remove also DictKey (use Object instead). --- Lib/test/picklecommon.py | 3 --- Lib/test/pickletester.py | 14 +------------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/Lib/test/picklecommon.py b/Lib/test/picklecommon.py index a8c8b92f9fb31d..bb8e41b01492ea 100644 --- a/Lib/test/picklecommon.py +++ b/Lib/test/picklecommon.py @@ -243,9 +243,6 @@ class MyUnicode(str): class MyUnicode(unicode): sample = unicode(r"hello \u1234", "raw-unicode-escape") -class DictKey: - pass - class MyTuple(tuple): sample = (1, 2, 3) diff --git a/Lib/test/pickletester.py b/Lib/test/pickletester.py index 3a1d0592e64155..0624b6a0257829 100644 --- a/Lib/test/pickletester.py +++ b/Lib/test/pickletester.py @@ -2913,7 +2913,7 @@ def test_recursive_frozenset_subclass_and_inst(self): def _test_recursive_collection_in_key(self, factory, minprotocol=0): protocols = range(minprotocol, pickle.HIGHEST_PROTOCOL + 1) - key = DictKey() + key = Object() o = factory({key: 1}) key.attr = o for proto in protocols: @@ -2924,12 +2924,6 @@ def _test_recursive_collection_in_key(self, factory, minprotocol=0): self.assertEqual(len(keys), 1) self.assertIs(keys[0].attr, x) - def test_recursive_dict_in_key(self): - self._test_recursive_collection_in_key(dict) - - def test_recursive_dict_subclass_in_key(self): - self._test_recursive_collection_in_key(MyDict) - def test_recursive_frozendict_in_key(self): self._test_recursive_collection_in_key(frozendict, minprotocol=2) @@ -2947,12 +2941,6 @@ def _test_recursive_collection_in_value(self, factory, minprotocol=0): self.assertEqual(len(x['key']), 1) self.assertIs(x['key'][0], x) - def test_recursive_dict_in_value(self): - self._test_recursive_collection_in_value(dict) - - def test_recursive_dict_subclass_in_value(self): - self._test_recursive_collection_in_value(MyDict) - def test_recursive_frozendict_in_value(self): self._test_recursive_collection_in_value(frozendict, minprotocol=2)