From d2fbcd81636c9d0fa9171d7f4b14cd0934bc8b75 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Tue, 17 Feb 2026 12:20:39 +0100 Subject: [PATCH 1/6] Implement copy and deepcopy for frozendict --- Lib/copy.py | 10 ++++++++-- Lib/test/test_copy.py | 13 +++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/Lib/copy.py b/Lib/copy.py index 4c024ab5311d2d..107089ec3323df 100644 --- a/Lib/copy.py +++ b/Lib/copy.py @@ -101,7 +101,7 @@ def copy(x): _copy_atomic_types = frozenset({types.NoneType, int, float, bool, complex, str, tuple, - bytes, frozenset, type, range, slice, property, + bytes, frozendict, frozenset, type, range, slice, property, types.BuiltinFunctionType, types.EllipsisType, types.NotImplementedType, types.FunctionType, types.CodeType, weakref.ref, super}) @@ -166,7 +166,7 @@ def deepcopy(x, memo=None): int, float, bool, complex, bytes, str, types.CodeType, type, range, types.BuiltinFunctionType, types.FunctionType, weakref.ref, property}) -_deepcopy_dispatch = d = {} +d = {} def _deepcopy_list(x, memo, deepcopy=deepcopy): @@ -203,10 +203,16 @@ def _deepcopy_dict(x, memo, deepcopy=deepcopy): return y d[dict] = _deepcopy_dict +def _deepcopy_frozendict(x, memo, deepcopy=deepcopy): + y = _deepcopy_dict(x, memo, deepcopy) + return frozendict(y) +d[frozendict] = _deepcopy_frozendict + def _deepcopy_method(x, memo): # Copy instance methods return type(x)(x.__func__, deepcopy(x.__self__, memo)) d[types.MethodType] = _deepcopy_method +_deepcopy_dispatch = frozendict(d) del d def _keep_alive(x, memo): diff --git a/Lib/test/test_copy.py b/Lib/test/test_copy.py index cfef24727e8c82..e61cf1d088bd7a 100644 --- a/Lib/test/test_copy.py +++ b/Lib/test/test_copy.py @@ -133,6 +133,12 @@ def test_copy_dict(self): self.assertEqual(y, x) self.assertIsNot(y, x) + def test_copy_frozendict(self): + x = frozendict(x=1, y=2) + self.assertIs(copy.copy(x), x) + x = frozendict() + self.assertIs(copy.copy(x), x) + def test_copy_set(self): x = {1, 2, 3} y = copy.copy(x) @@ -419,6 +425,13 @@ def test_deepcopy_dict(self): self.assertIsNot(x, y) self.assertIsNot(x["foo"], y["foo"]) + def test_deepcopy_frozendict(self): + x = {"foo": [1, 2], "bar": 3} + y = copy.deepcopy(x) + self.assertEqual(y, x) + self.assertIsNot(x, y) + self.assertIsNot(x["foo"], y["foo"]) + @support.skip_emscripten_stack_overflow() @support.skip_wasi_stack_overflow() def test_deepcopy_reflexive_dict(self): From 8399c49d91c90739ed03e71f0213b4922fde26f1 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 11:28:46 +0000 Subject: [PATCH 2/6] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2026-02-17-11-28-37.gh-issue-141510.OpAz0M.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-02-17-11-28-37.gh-issue-141510.OpAz0M.rst diff --git a/Misc/NEWS.d/next/Library/2026-02-17-11-28-37.gh-issue-141510.OpAz0M.rst b/Misc/NEWS.d/next/Library/2026-02-17-11-28-37.gh-issue-141510.OpAz0M.rst new file mode 100644 index 00000000000000..5b604124c6d7cc --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-02-17-11-28-37.gh-issue-141510.OpAz0M.rst @@ -0,0 +1,2 @@ +The :mod:`copy` module now supports the :class:`frozendict` type. Patch by +Pieter Eendebak based on work by Victor Stinner. From d3ee7d16e92035341c14ad1078260f9a78b4881f Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Tue, 17 Feb 2026 21:28:10 +0100 Subject: [PATCH 3/6] adjust tests --- Lib/test/test_descr.py | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py index d6e3719479a214..a6d72444317dea 100644 --- a/Lib/test/test_descr.py +++ b/Lib/test/test_descr.py @@ -311,12 +311,7 @@ def test_spam_lists(self): # Testing spamlist operations... import copy, xxsubtype as spam - def spamlist(l, memo=None): - import xxsubtype as spam - return spam.spamlist(l) - - # This is an ugly hack: - copy._deepcopy_dispatch[spam.spamlist] = spamlist + spamlist = spam.spamlist self.binop_test(spamlist([1]), spamlist([2]), spamlist([1,2]), "a+b", "__add__") @@ -350,19 +345,22 @@ def foo(self): return 1 a.setstate(42) self.assertEqual(a.getstate(), 42) + # test deepcopy makes a deep copy of the elements of a spamlist + a.append([]) + a_copy = copy.deepcopy(a) + assert a_copy is not a + assert a_copy[-1] is not a[-1] + assert a_copy == a + # the spamlist does not explictly implement deepcopy, the state is not copied + assert a_copy.getstate() != a.getstate() + @support.impl_detail("the module 'xxsubtype' is internal") @unittest.skipIf(xxsubtype is None, "requires xxsubtype module") def test_spam_dicts(self): # Testing spamdict operations... import copy, xxsubtype as spam - def spamdict(d, memo=None): - import xxsubtype as spam - sd = spam.spamdict() - for k, v in list(d.items()): - sd[k] = v - return sd - # This is an ugly hack: - copy._deepcopy_dispatch[spam.spamdict] = spamdict + + spamdict = spam.spamdict self.binop_test(spamdict({1:2,3:4}), 1, 1, "b in a", "__contains__") self.binop_test(spamdict({1:2,3:4}), 2, 0, "b in a", "__contains__") @@ -375,7 +373,9 @@ def spamdict(d, memo=None): for i in iter(d): l.append(i) self.assertEqual(l, l1) + l = [] + for i in d.__iter__(): l.append(i) self.assertEqual(l, l1) @@ -389,6 +389,7 @@ def spamdict(d, memo=None): self.unop_test(spamd, repr(straightd), "repr(a)", "__repr__") self.set2op_test(spamdict({1:2,3:4}), 2, 3, spamdict({1:2,2:3,3:4}), "a[b]=c", "__setitem__") + # Test subclassing class C(spam.spamdict): def foo(self): return 1 @@ -401,6 +402,15 @@ def foo(self): return 1 a.setstate(100) self.assertEqual(a.getstate(), 100) + # test deepcopy makes a deep copy of the elements of a spamdict + a[-1] = [] + a_copy = copy.deepcopy(a) + assert a_copy is not a + assert a_copy[-1] is not a[-1] + assert a_copy == a + # the spamdict does not explictly implement deepcopy, the state is not copied + assert a_copy.getstate() != a.getstate() + def test_wrap_lenfunc_bad_cast(self): self.assertEqual(range(sys.maxsize).__len__(), sys.maxsize) From b0729ae82fe4f2c7a4536826f1a0d6f0b52631a1 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Tue, 17 Feb 2026 21:33:30 +0100 Subject: [PATCH 4/6] Update Lib/test/test_copy.py Co-authored-by: Victor Stinner --- Lib/test/test_copy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_copy.py b/Lib/test/test_copy.py index e61cf1d088bd7a..858e5e089d5aba 100644 --- a/Lib/test/test_copy.py +++ b/Lib/test/test_copy.py @@ -426,7 +426,7 @@ def test_deepcopy_dict(self): self.assertIsNot(x["foo"], y["foo"]) def test_deepcopy_frozendict(self): - x = {"foo": [1, 2], "bar": 3} + x = frozendict({"foo": [1, 2], "bar": 3}) y = copy.deepcopy(x) self.assertEqual(y, x) self.assertIsNot(x, y) From 4efeee370a61b38440c637a67895b5cd36b9ddfa Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Tue, 17 Feb 2026 21:35:29 +0100 Subject: [PATCH 5/6] whitespace --- Lib/test/test_descr.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py index a6d72444317dea..94b81f9e134aec 100644 --- a/Lib/test/test_descr.py +++ b/Lib/test/test_descr.py @@ -373,9 +373,7 @@ def test_spam_dicts(self): for i in iter(d): l.append(i) self.assertEqual(l, l1) - l = [] - for i in d.__iter__(): l.append(i) self.assertEqual(l, l1) @@ -389,7 +387,6 @@ def test_spam_dicts(self): self.unop_test(spamd, repr(straightd), "repr(a)", "__repr__") self.set2op_test(spamdict({1:2,3:4}), 2, 3, spamdict({1:2,2:3,3:4}), "a[b]=c", "__setitem__") - # Test subclassing class C(spam.spamdict): def foo(self): return 1 From 2dfc60a79c04ec571cb7b93bdb7408cf645f4443 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Tue, 17 Feb 2026 22:03:16 +0100 Subject: [PATCH 6/6] review comments --- Lib/copy.py | 3 +-- Lib/test/test_descr.py | 35 ++++++++++++++--------------------- 2 files changed, 15 insertions(+), 23 deletions(-) diff --git a/Lib/copy.py b/Lib/copy.py index 107089ec3323df..33dabb3395a7c0 100644 --- a/Lib/copy.py +++ b/Lib/copy.py @@ -166,7 +166,7 @@ def deepcopy(x, memo=None): int, float, bool, complex, bytes, str, types.CodeType, type, range, types.BuiltinFunctionType, types.FunctionType, weakref.ref, property}) -d = {} +_deepcopy_dispatch = d = {} def _deepcopy_list(x, memo, deepcopy=deepcopy): @@ -212,7 +212,6 @@ def _deepcopy_method(x, memo): # Copy instance methods return type(x)(x.__func__, deepcopy(x.__self__, memo)) d[types.MethodType] = _deepcopy_method -_deepcopy_dispatch = frozendict(d) del d def _keep_alive(x, memo): diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py index 94b81f9e134aec..d6e3719479a214 100644 --- a/Lib/test/test_descr.py +++ b/Lib/test/test_descr.py @@ -311,7 +311,12 @@ def test_spam_lists(self): # Testing spamlist operations... import copy, xxsubtype as spam - spamlist = spam.spamlist + def spamlist(l, memo=None): + import xxsubtype as spam + return spam.spamlist(l) + + # This is an ugly hack: + copy._deepcopy_dispatch[spam.spamlist] = spamlist self.binop_test(spamlist([1]), spamlist([2]), spamlist([1,2]), "a+b", "__add__") @@ -345,22 +350,19 @@ def foo(self): return 1 a.setstate(42) self.assertEqual(a.getstate(), 42) - # test deepcopy makes a deep copy of the elements of a spamlist - a.append([]) - a_copy = copy.deepcopy(a) - assert a_copy is not a - assert a_copy[-1] is not a[-1] - assert a_copy == a - # the spamlist does not explictly implement deepcopy, the state is not copied - assert a_copy.getstate() != a.getstate() - @support.impl_detail("the module 'xxsubtype' is internal") @unittest.skipIf(xxsubtype is None, "requires xxsubtype module") def test_spam_dicts(self): # Testing spamdict operations... import copy, xxsubtype as spam - - spamdict = spam.spamdict + def spamdict(d, memo=None): + import xxsubtype as spam + sd = spam.spamdict() + for k, v in list(d.items()): + sd[k] = v + return sd + # This is an ugly hack: + copy._deepcopy_dispatch[spam.spamdict] = spamdict self.binop_test(spamdict({1:2,3:4}), 1, 1, "b in a", "__contains__") self.binop_test(spamdict({1:2,3:4}), 2, 0, "b in a", "__contains__") @@ -399,15 +401,6 @@ def foo(self): return 1 a.setstate(100) self.assertEqual(a.getstate(), 100) - # test deepcopy makes a deep copy of the elements of a spamdict - a[-1] = [] - a_copy = copy.deepcopy(a) - assert a_copy is not a - assert a_copy[-1] is not a[-1] - assert a_copy == a - # the spamdict does not explictly implement deepcopy, the state is not copied - assert a_copy.getstate() != a.getstate() - def test_wrap_lenfunc_bad_cast(self): self.assertEqual(range(sys.maxsize).__len__(), sys.maxsize)