Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cannot call replace() on a dataclass with InitVar

Defining the following subclass:

from dataclasses import dataclass, replace, field, InitVar

@dataclass
class MyDataClass:
    foo: InitVar[str]
    bar: str
    foo_len: int = field(init=False)
    def __post_init__(self, foo: int):
        self.foo_len = len(foo)

And creating an instance of it:

instance = MyDataClass(foo="foo", bar="bar")

Trying to call replace on the instance fails:

In[5]: replace(instance, bar="baz")

Traceback (most recent call last):
  File "/home/or/.venv/m/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 3343, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-5-5de186c91dc9>", line 1, in <module>
    replace(instance, bar="baz")
  File "/home/or/.venv/m/lib/python3.6/site-packages/dataclasses.py", line 1170, in replace
    changes[f.name] = getattr(obj, f.name)
AttributeError: 'MyDataClass' object has no attribute 'foo'

As far as I understand, InitVars should exist during init only, but from some investigation, I can see they are put into __dataclass_fields__ and therefore replace is trying to use them.

I'm using Python 3.6, so my dataclasses package is the backport, not the built-in package for Python 3.7+.

I found this line in dataclasses' documentation:

Init-only variables without default values, if any exist, must be specified on the call to replace() so that they can be passed to init() and post_init().

which means if I had the original value of instance.foo, I could theoretically do:

replace(instance, bar="baz", foo="value of foo")

But I don't have the original value of instance.foo.

How can I avoid this error if I want to create a copy of an existing instance of a dataclass?

like image 474
Or B Avatar asked Apr 29 '26 22:04

Or B


1 Answers

Having InitVars in your dataclass means that you can't call the constructor without explicitly passing the InitVar again. This disqualifies the usage of both replace(instance, ...) as well as MyDataClass(**asdict(instance), ...) if you don't have access to the InitVar any more.

If all you care about is getting a valid copy you can use the standard-librarie's copy.copy though, (or copy.deepcopy for dataclasses that have their own containers, or are otherwise nested) which does not call the instance's constructor:

>>> from copy import copy
>>> instance_a = MyDataClass(foo="foo", bar="bar")
>>> instance_b = copy(instance_a)
>>> instance_b
MyDataClass(bar='bar', foo_len=3)
>>> instance_a is instance_b
False
like image 200
Arne Avatar answered May 02 '26 10:05

Arne



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!