diff --git a/Lib/copy.py b/Lib/copy.py index 6149301ad1389e..ac15c439f22ebe 100644 --- a/Lib/copy.py +++ b/Lib/copy.py @@ -59,6 +59,8 @@ class Error(Exception): __all__ = ["Error", "copy", "deepcopy", "replace"] +_MEMO_MISS = object() + def copy(x): """Shallow copy operation on arbitrary Python objects. @@ -122,8 +124,8 @@ def deepcopy(x, memo=None): if memo is None: memo = {} else: - y = memo.get(d, None) - if y is not None: + y = memo.get(d, _MEMO_MISS) + if y is not _MEMO_MISS: return y copier = _deepcopy_dispatch.get(cls) diff --git a/Lib/test/test_copy.py b/Lib/test/test_copy.py index 9455c9e00514ca..42560d0342d585 100644 --- a/Lib/test/test_copy.py +++ b/Lib/test/test_copy.py @@ -939,6 +939,19 @@ def m(self): self.assertIs(g.b.__self__, g) g.b() + def test_deepcopy_memo_none_result(self): + # Objects whose deepcopy result is None must still be memoized. + call_count = 0 + class C: + def __deepcopy__(self, memo): + nonlocal call_count + call_count += 1 + memo[id(self)] = None + return None + obj = C() + copy.deepcopy([obj, obj, obj]) + self.assertEqual(call_count, 1) + class TestReplace(unittest.TestCase): diff --git a/Misc/NEWS.d/next/Library/2026-07-24-00-02-00.gh-issue-154594.Xk7mQ2.rst b/Misc/NEWS.d/next/Library/2026-07-24-00-02-00.gh-issue-154594.Xk7mQ2.rst new file mode 100644 index 00000000000000..f04b06e82c55db --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-24-00-02-00.gh-issue-154594.Xk7mQ2.rst @@ -0,0 +1,3 @@ +Fix :func:`copy.deepcopy` memo lookup using ``None`` as the miss sentinel, +which prevented memoization of objects whose deep copy result is ``None``. +Use a private sentinel object instead. Patch by tonghuaroot.