This issue tracker has been migrated to GitHub, and is currently read-only.
For more information, see the GitHub FAQs in the Python's Developer Guide.

classification
Title: dataclass init=False field with default works but default_factory does not
Type: behavior Stage:
Components: ctypes Versions: Python 3.9
process
Status: open Resolution:
Dependencies: Superseder:
Assigned To: Nosy List: andrei.avk, eric.smith, simple_coder878, sobolevn
Priority: normal Keywords:

Created on 2021-10-04 20:17 by simple_coder878, last changed 2022-04-11 14:59 by admin.

Messages (4)
msg403182 - (view) Author: simple_coder878 (simple_coder878) Date: 2021-10-04 20:17
Simple example
from dataclasses import dataclass, field

@dataclass(init=False)
class TestObject(object):
    m: str = field(default='hi')
    k: list = field(default_factory=list)

    def test(self):
        print(f'm is {self.m} ')
        print(f'k is {self.k}')

if __name__ == '__main__':
    myobject = TestObject()
    myobject.test()


Produces:

Traceback (most recent call last):
  File "H:\unit_test\tests_dataclass.py", line 81, in <module>
    myobject.test()
  File "H:\unit_test\tests_dataclass.py", line 76, in test
    print(f'k is {self.k}')
AttributeError: 'TestObject' object has no attribute 'k'
m is hi 

So m is initialized to hi but k just disappears

But wait there's more!

If i do 

from dataclasses import dataclass, field

@dataclass(init=False)
class TestObject(object):
    m: str = field(default='hi')
    k: list = field(default_factory=list)

    def test(self):
        print(f'm is {self.m} ')
        print(f'k is {self.k}')


@dataclass
class InheritedTestObject(TestObject):

    def __post_init__(self):
        super().__init__()
        print(f'Inherited m is {self.m} ')
        print(f'Inherited k is {self.k}')
        print(f'Inherited g is {self.k}')


if __name__ == '__main__':
    myobject = InheritedTestObject()
    myobject.test()

It produces:

Inherited m is hi 
Inherited k is []
Inherited g is []
m is hi 
k is []

Process finished with exit code 0

NO ERRORS!

It seems like a bug to me, but what is the expected behavior in this case? I would expect the first case to not error out and should have an empty list.

I've only tested this on Python 3.9 so far.
msg403183 - (view) Author: simple_coder878 (simple_coder878) Date: 2021-10-04 20:31
Also wanted to add that I did try another variation of the first example where I set the default_factory field's init value to False and I got the same error.

from dataclasses import dataclass, field

@dataclass(init=False)
class TestObject(object):
    m: str = field(default='hi')
    k: list = field(init=False, default_factory=list)

    def test(self):
        print(f'm is {self.m} ')
        print(f'k is {self.k}')

if __name__ == '__main__':
    myobject = TestObject()
    myobject.test()

Also produces 

Traceback (most recent call last):
  File "H:\unit_test\tests_dataclass.py", line 81, in <module>
    myobject.test()
  File "H:\unit_test\tests_dataclass.py", line 76, in test
    print(f'k is {self.k}')
AttributeError: 'TestObject' object has no attribute 'k'
m is hi
msg403206 - (view) Author: Nikita Sobolev (sobolevn) * (Python triager) Date: 2021-10-05 09:10
Right now `dataclasses.py` has this explanation: https://github.com/python/cpython/blame/07cf10bafc8f6e1fcc82c10d97d3452325fc7c04/Lib/dataclasses.py#L962-L966

```
         if isinstance(getattr(cls, f.name, None), Field):
            if f.default is MISSING:
                # If there's no default, delete the class attribute.
                # This happens if we specify field(repr=False), for
                # example (that is, we specified a field object, but
                # no default value).  Also if we're using a default
                # factory.  The class attribute should not be set at
                # all in the post-processed class.
                delattr(cls, f.name)
            else:
                setattr(cls, f.name, f.default)
```

This is why: imagine, that we execute `.default_factory` there.
It will be vulnerable to "default mutable" problem:

```
from dataclasses import dataclass, field

@dataclass(init=False)
class TestObject(object):
    m: str = field(default='hi')
    k: list = field(default_factory=list)

    def test(self):
        print(f'm is {self.m} ')
        self.k.append(1)
        print(f'k is {self.k}')

if __name__ == '__main__':
    myobject = TestObject()
    print(TestObject.m)  # hi
    print(TestObject.k)  # []

    myobject.test()
    # m is hi 
    # k is [1]

    other_object = TestObject()
    other_object.test()
    # m is hi 
    # k is [1, 1]
```

Another, more complex solution is to track fields with `default_factory` and still generate `__init__` / `__new__` / etc for them to run their `default_factories`s when object is created.
msg405920 - (view) Author: Andrei Kulakov (andrei.avk) * (Python triager) Date: 2021-11-08 01:28
I think a good possible solution is to raise an error if `default_factory` is provided on a `init=False` dataclass that doesn't have a `__init__()` defined. However, it will create a slight inconsistency because there will be an error when `__init__` is not defined, but no error otherwise -- to allow calling the factory in custom defined `__init__()`.

The error would be "Error: default_factory argument needs `init=True` to be set on dataclass because default_factory value is created in the generated __init__() method".
History
Date User Action Args
2022-04-11 14:59:50adminsetgithub: 89529
2021-11-08 01:28:02andrei.avksetnosy: + andrei.avk
messages: + msg405920
2021-10-05 09:10:15sobolevnsetnosy: + sobolevn
messages: + msg403206
2021-10-05 03:55:55xtreaksetnosy: + eric.smith
2021-10-04 20:31:56simple_coder878setmessages: + msg403183
2021-10-04 20:17:33simple_coder878create