You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
maximum recursion depth in deepcopy regression #126817
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 requiredimportcopyclassTest:
def__init__(self, a, b):
self.a=aself.b=aself._state=Truedef__deepcopy__(self, memo=None):
p=copy.deepcopy(super(), memo)
p._state=Falsereturnpt=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.
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? 🤔
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.
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:
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.
Activity
serhiy-storchaka commentedon Nov 18, 2024
Interesting. I did not foresee such use case.
In general, such code worked by accident, because the code in the
copymodule is more permissive than in thepicklemodule. 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? 🤔tacaswell commentedon Nov 19, 2024
sounds like we should find another way downstream no matter which way CPython goes on this question?
tacaswell commentedon Dec 31, 2024
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
That is now failing with
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 whatsuper().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
superobjects should work or not (I think a compelling case could be made for pickle simply refusing to tray), but feel pretty strongly thatcopy.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.FIX: account for changes to cpython with copy/deepcopy to super()
tacaswell commentedon Dec 31, 2024
matplotlib/matplotlib#29393 is a hacky work-around to this change.
FIX: account for changes to cpython with copy/deepcopy to super()
FIX: account for changes to cpython with copy/deepcopy to super()
tacaswell commentedon Jan 30, 2025
@serhiy-storchaka Can this be labeled so it is resolved one way or the other by 3.14.0b1?
FIX: account for changes to cpython with copy/deepcopy to super()
tacaswell commentedon Apr 27, 2025
@serhiy-storchaka At this point should I assume that this will be the state in 3.14 and start to address the change downstream?
FIX: account for changes to cpython with copy/deepcopy to super()
mdboom commentedon Apr 28, 2025
We should probably determine whether we can/should fix or add a note to WHATSNEW.
FIX: account for changes to cpython with copy/deepcopy to super()
jkseppan commentedon Jun 23, 2025
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 callcopy.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.deepcopycalls after it has done its job. Without such a hook, I suppose the best option is to reimplementdeepcopywithout calling it on the original object:matplotlib/matplotlib#30198
serhiy-storchaka commentedon Jun 23, 2025
copy.deepcopy()itselfs ensures thatmemopassed to__deepcopy__()is not None, and test if the object is already inmemo, so__deepcopy__()should not do this.If there is something that can help, we can add it to the
copymodule. But reverting the behavior ofsuper()is not right solution. And I have no other ideas. So this behavior will remain.