Skip to content

maximum recursion depth in deepcopy regression #126817

Closed as not planned
Closed as not planned

Description

@tacaswell

Bug report

Bug description:

The following code works on py313 and below (this is a very cut-down version of some code in Matplotilb (matplotlib.path.Path)) but hit the maximum recursion depth on main.

# Add a code block here, if required
import copy

class Test:
    def __init__(self, a, b):
        self.a = a
        self.b = a
        self._state = True

    def __deepcopy__(self, memo=None):
        p = copy.deepcopy(super(), memo)
        p._state = False
        return p

t = Test(1, 2)
t2 = copy.deepcopy(t)

I bisected this to #125781 and suspect the problem is that because it is returning a new object it is escaping the memoization and looping.

I'm happy to be told we should not be doing this fix it on the Matplotlib side, but I would like to be sure that this was an expected consequence of the change.

attn @serhiy-storchaka

CPython versions tested on:

3.14, CPython main branch

Operating systems tested on:

Linux

Activity

  1. added
    stdlibStandard Library Python modules in the Lib/ directory
    on Nov 14, 2024
  2. serhiy-storchaka commented on Nov 18, 2024

    @serhiy-storchaka
    Member

    Interesting. I did not foresee such use case.

    In general, such code worked by accident, because the code in the copy module is more permissive than in the pickle module. Although it does not work if the reducing function returns a string (enums or other singletons). It does not work if copying in the parent class was implemented by the __copy__ and __deepcopy__ methods.

    The question is how to support that use case -- copying an object ignoring the special methods in the current class. Should we use super() for this? How to resolve conflict with different semantic for pickling? 🤔

  3. tacaswell commented on Nov 19, 2024

    @tacaswell
    ContributorAuthor

    sounds like we should find another way downstream no matter which way CPython goes on this question?

  4. tacaswell commented on Dec 31, 2024

    @tacaswell
    ContributorAuthor

    Another example from Matplotlib that I suspect is coming from the same cause (but have not actually bisected) is

    https://github.com/matplotlib/matplotlib/blob/f2717a5486f782baca9fedd5fdb28f73e6c366d0/lib/matplotlib/transforms.py#L143-L153

        def __copy__(self):
            other = copy.copy(super())
            # If `c = a + b; a1 = copy(a)`, then modifications to `a1` do not
            # propagate back to `c`, i.e. we need to clear the parents of `a1`.
            other._parents = {}
            # If `c = a + b; c1 = copy(c)`, then modifications to `a` also need to
            # be propagated to `c1`.
            for key, val in vars(self).items():
                if isinstance(val, TransformNode) and id(self) in val._parents:
                    other.set_children(val)  # val == getattr(other, key)
            return other
    

    That is now failing with

    E       AttributeError: 'super' object has no attribute '_parents' and no __dict__ for setting new attributes
    

    It seems right to be to be able to access "copy/deepcopy this as if the object were its parent class" via copy.deepcopy(super()) because for all other methods that is what super().foo() does. A lazy (but apparently wrong) way to think about copy/deepcopy is "please invoke __copy__ / __deepcopy__ for me".

    I'm indifferent on if pickling super objects should work or not (I think a compelling case could be made for pickle simply refusing to tray), but feel pretty strongly that copy.deepcopy(super()) when used in __deepcopy__ should return a copy of the calling object as if it were the next class in the mro. Otherwise it is not clear how we should be extending a base classes copying implementation.

  5. tacaswell commented on Dec 31, 2024

    @tacaswell
    ContributorAuthor

    matplotlib/matplotlib#29393 is a hacky work-around to this change.

  6. tacaswell commented on Jan 30, 2025

    @tacaswell
    ContributorAuthor

    @serhiy-storchaka Can this be labeled so it is resolved one way or the other by 3.14.0b1?

  7. tacaswell commented on Apr 27, 2025

    @tacaswell
    ContributorAuthor

    @serhiy-storchaka At this point should I assume that this will be the state in 3.14 and start to address the change downstream?

  8. mdboom commented on Apr 28, 2025

    @mdboom
    Contributor

    We should probably determine whether we can/should fix or add a note to WHATSNEW.

  9. jkseppan commented on Jun 23, 2025

    @jkseppan

    So if I understand this right, Matplotlib wants to customize deep copies of objects, so that they are copied in the usual way except that one specific attribute is always reset in the copy. To do that, the class needs to define a __deepcopy__ method, but then it is not obvious how to get the unmodified deep copy in that method. The call copy.deepcopy(super()) used to produce a normal deep copy while not calling __deepcopy__ recursively, but this no longer works.

    Ideally there would be a way to define a hook that copy.deepcopy calls after it has done its job. Without such a hook, I suppose the best option is to reimplement deepcopy without calling it on the original object:

    matplotlib/matplotlib#30198

    import copy
    
    class Test:
        def __init__(self, a, b):
            self.a = a
            self.b = b
            self._state = True
    
        def __deepcopy__(self, memo=None):
            if memo is None:
                memo = {}
            if id(self) in memo:
                return memo[id(self)]
    
            cls = type(self)
            p = cls.__new__(cls)
            memo[id(self)] = p
    
            for k, v in self.__dict__.items():
                setattr(p, k, copy.deepcopy(v, memo))
    
            p._state = False
            return p
    
    t = Test(1, 2)
    t2 = copy.deepcopy(t)
    print(t._state, t2._state)   # True False
  10. serhiy-storchaka commented on Jun 23, 2025

    @serhiy-storchaka
    Member

    copy.deepcopy() itselfs ensures that memo passed to __deepcopy__() is not None, and test if the object is already in memo, so __deepcopy__() should not do this.

    If there is something that can help, we can add it to the copy module. But reverting the behavior of super() is not right solution. And I have no other ideas. So this behavior will remain.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    stdlibStandard Library Python modules in the Lib/ directorytype-bugAn unexpected behavior, bug, or error

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions