Skip to content

Copying interned object via copy #146323

@Red4Ru

Description

@Red4Ru

Bug report

Bug description:

Just found a weird behavior with objects interned via caching __new__ result: __new__ is called with no additional arguments during copying which leads to unexpected behavior (all copies become the same object)

import copy
from functools import cache

class Dummy:
    def __init__(self, field):
        self.field = field
    @cache
    def __new__(cls, *args, **kwargs):
        print(f'__new__ is called with {args=} {kwargs=}')
        return super(Dummy, cls).__new__(cls)

a, b, c = Dummy('a'), Dummy('b'), Dummy('c')
# __new__ is called with args=('a',) kwargs={}
# __new__ is called with args=('b',) kwargs={}
# __new__ is called with args=('c',) kwargs={}
print(vars(a), vars(b), vars(c))
# {'field': 'a'} {'field': 'b'} {'field': 'c'}
dc_a = copy.deepcopy(a)
# __new__ is called with args=() kwargs={}
print(vars(dc_a))
# {'field': 'a'}
dc_b = copy.deepcopy(b)
print(vars(dc_a), vars(dc_b))
# {'field': 'b'} {'field': 'b'}
dc_a is dc_b
# True
dc_c = copy.deepcopy(c)
print(vars(dc_a), vars(dc_b), vars(dc_c))
# {'field': 'c'} {'field': 'c'} {'field': 'c'}
dc_c is dc_b
# True

# with copy.copy works too
c_a = copy.copy(a)
c_b = copy.copy(b)
c_c = copy.copy(c)
print(vars(c_a), vars(c_b), vars(c_c))
# {'field': 'c'} {'field': 'c'} {'field': 'c'}
c_a is dc_a
# True
c_c is c
# False

It works with frozen sloted dataclasses as well

@dataclass(slots=True, frozen=True)
class Dummy:
    str_field: str
    int_field: int
    @cache
    def __new__(cls, *args, **kwargs):
        print(f'__new__ called with {args=} {kwargs=}')
        return super(Dummy, cls).__new__(cls)
Dummy('a', 1) is Dummy('a', 1)
# __new__ called with args=('a', 1) kwargs={}
# True
Dummy('a', 1) is not Dummy('b', 1)
# __new__ called with args=('b', 1) kwargs={}
# True
Dummy('a', 1) is copy.deepcopy(Dummy('a', 1))
# __new__ called with args=() kwargs={}
# False
copy.deepcopy(Dummy('a', 1)) is copy.deepcopy(Dummy('b', 1))
# True

Yes, there is a workaround requiring to define something like this in the class

def __deepcopy__(self, memo=None):
    return self

But it still looks like a bug.
If it is somehow an intended behavior it would be useful to point that out here: https://docs.python.org/3/library/copy.html

CPython versions tested on:

3.14

Operating systems tested on:

macOS

Metadata

Metadata

Assignees

No one assigned

    Labels

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

    Projects

    Status

    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions