diff --git a/Lib/copy.py b/Lib/copy.py index 6149301ad1389e2..b7fcccebd370ec7 100644 --- a/Lib/copy.py +++ b/Lib/copy.py @@ -122,9 +122,10 @@ def deepcopy(x, memo=None): if memo is None: memo = {} else: - y = memo.get(d, None) - if y is not None: - return y + try: + return memo[d] + except KeyError: + pass copier = _deepcopy_dispatch.get(cls) if copier is not None: diff --git a/Lib/test/test_copy.py b/Lib/test/test_copy.py index 98f56b5ae87f964..c9be475542ce70d 100644 --- a/Lib/test/test_copy.py +++ b/Lib/test/test_copy.py @@ -509,6 +509,22 @@ def __eq__(self, other): self.assertIsNot(y, x) self.assertIsNot(y.foo, x.foo) + def test_deepcopy_inst_deepcopy_returns_none(self): + # gh-154594: the memo used to use None as its own "not cached" + # sentinel, so a __deepcopy__ that legitimately returns None was + # indistinguishable from a memo miss and got invoked again on + # every subsequent reference to the same object. + calls = [] + class C: + def __deepcopy__(self, memo): + calls.append(1) + memo[id(self)] = None + return None + x = C() + y = copy.deepcopy([x, x, x]) + self.assertEqual(y, [None, None, None]) + self.assertEqual(len(calls), 1) + def test_deepcopy_inst_getinitargs(self): class C: def __init__(self, foo): diff --git a/Misc/NEWS.d/next/Library/2026-07-28-10-00-00.gh-issue-154594.lRVvrN.rst b/Misc/NEWS.d/next/Library/2026-07-28-10-00-00.gh-issue-154594.lRVvrN.rst new file mode 100644 index 000000000000000..9de7191f708edea --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-28-10-00-00.gh-issue-154594.lRVvrN.rst @@ -0,0 +1,3 @@ +Fix :func:`copy.deepcopy` re-invoking an object's :meth:`~object.__deepcopy__` +on every subsequent reference to that object when it returns :const:`None`. +The memo dict now distinguishes a cached ``None`` result from a cache miss.