66

Let's assume you have defined a Python dataclass:

@dataclass
class Marker:
    a: float
    b: float = 1.0

What's the easiest way to copy the values from an instance marker_a to another instance marker_b?

Here's an example of what I try to achieve:

marker_a = Marker(1.0, 2.0)
marker_b = Marker(11.0, 12.0)
# now some magic happens which you hopefully can fill in
print(marker_b)
# result: Marker(a=1.0, b=2.0)

As a boundary condition, I do not want to create and assign a new instance to marker_b.

OK, I could loop through all defined fields and copy the values one by one, but there has to be a simpler way, I guess.

Tom Pohl
  • 2,711
  • 3
  • 27
  • 34
  • 2
    For newcomers: use `dataclasses.replace` as shown in https://stackoverflow.com/a/63648003/362021 – Malcolm May 24 '22 at 23:10
  • @Malcolm Actually, `dataclasses.replace` wouldn't have worked for me. I had several UI callbacks pointing to methods of my dataclass instance. That's why I specifically asked for _not_ a new instance. – Tom Pohl May 25 '22 at 05:50
  • Ah, I missed that you want mutation. – Malcolm Jun 03 '22 at 19:57
  • I made the boundary condition more prominent as it was easy to miss. – Tom Pohl Jun 04 '22 at 14:55

5 Answers5

123

The dataclasses.replace function returns a new copy of the object. Without passing in any changes, it will return a copy with no modification:

>>> import dataclasses
>>> @dataclasses.dataclass
... class Dummy:
...     foo: int
...     bar: int
... 
>>> dummy = Dummy(1, 2)
>>> dummy_copy = dataclasses.replace(dummy)
>>> dummy_copy.foo = 5
>>> dummy
Dummy(foo=1, bar=2)
>>> dummy_copy
Dummy(foo=5, bar=2)

Note that this is a shallow copy.

Edit to address comments:

If a copy is undesirable, I would probably go with the following:

for key, value in dataclasses.asdict(dummy).items():
    setattr(some_obj, key, value)
CharlesB
  • 86,532
  • 28
  • 194
  • 218
Daniel Perez
  • 6,335
  • 4
  • 24
  • 28
  • 4
    The question is specifically about how to copy the fields into an existing instance. – Christopher Barber Jun 11 '21 at 18:40
  • The problem is that my data class has additional methods which are used in callbacks. That's why I don't want to create a new instance of the data class object. – Tom Pohl Jun 15 '21 at 14:32
  • 5
    This is not a direct answer to the question, but can be found in Google as an answer to the question of how to create a copy, so thank you for this answer anyway! – Tigran Saluev Aug 08 '21 at 12:26
  • Too bad this isn't type safe (yet?). At least as of now `mypy` doesn't catch misspelled attribute names or types. – bluenote10 Sep 24 '21 at 10:17
  • 1
    Note that the added edit which uses `asdict` will convert any sub-dataclass instance to dictionary as it is recursive! – Nova Jul 24 '23 at 22:14
12

I think that looping over the fields probably is the easiest way. All the other options I can think of involve creating a new object.

from dataclasses import fields

marker_a = Marker(5)
marker_b = Marker(0, 99)

for field in fields(Marker):
    setattr(marker_b, field.name, getattr(marker_a, field.name))

print(marker_b)  # Marker(a=5, b=1.0)
Patrick Haugh
  • 59,226
  • 13
  • 88
  • 96
  • Thanks. It's similar to what I'm doing right now, but it somehow feels un-pythonic to me. – Tom Pohl Sep 16 '19 at 19:05
  • It's a bit of an unusual use case. The dataclasses module seems to mostly assume that you'll be happy making a new object. I would recommend sticking this (or whatever you have) in a function and moving on. One thing that's worth thinking about is what you want to happen if one of your arguments is actually a subclass of `Marker` with additional fields. – Patrick Haugh Sep 16 '19 at 19:08
7
@dataclass
class Marker:
    a: float
    b: float = 1.0

marker_a = Marker(0.5)

marker_b = Marker(**marker_a.__dict__)

marker_b

# Marker(a=0.5, b=1.0)

If you didn't want to create a new instance, try this:

marker_a = Marker(1.0, 2.0)
marker_b = Marker(11.0, 12.0)

marker_b.__dict__ = marker_a.__dict__.copy()

# result: Marker(a=1.0, b=2.0)

Not sure whether that's considered a bad hack though...

r.ook
  • 13,466
  • 2
  • 22
  • 39
4

Another option which may be more elegant:

import dataclasses

marker_a = Marker(1.0, 2.0)
marker_b = Marker(**dataclasses.asdict(marker_a))
Nur L
  • 791
  • 7
  • 15
  • 1
    Thanks, but my boundary condition was to _not_ create a new instance. In my special case I have other object point to this particular instance, so a new instance is not an option. – Tom Pohl Oct 10 '20 at 19:44
0

Here's a version that also lets you choose the result dataclass type and override attributes:

dataclassWith(Y(x=2, z=5), y=3)      # > Y(x=3, y=3, z=5)
dataclassWith(Y(x=2, z=5), X, x=99)  # > X(z=5, x=99)  # There is no z
MISSING = object()
def dataclassWith(other, clz=None, **kw):
    if clz is None: clz = other.__class__

    k = other.__dict__.copy()
    k.update(kw)
    return clz(**{k:v for k,v in k.items()
                  if getattr(clz, k, MISSING) is not MISSING})


class TestDataclassUtil(unittest.TestCase):
    def test_dataclassWith(self):
        @dataclasses.dataclass
        class X():
            x:int = 1
            z:int = 99

        @dataclasses.dataclass
        class Y(X):
            y:int = 2

        r = dataclassWith(Y(x=2), y=3)
        self.assertTrue(isinstance(r, Y))
        self.assertTrue(r.x==2)
        self.assertTrue(r.y==3)
        self.assertTrue(r.z==99)

        r = dataclassWith(Y(x=2), X, z=100)
        self.assertTrue(isinstance(r, X))
        self.assertTrue(r.x==2)
        self.assertTrue(r.z==100)
user48956
  • 14,850
  • 19
  • 93
  • 154