Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dataclass init=False field with default works but default_factory does not #89529

Open
simplecoder878 mannequin opened this issue Oct 4, 2021 · 4 comments
Open

dataclass init=False field with default works but default_factory does not #89529

simplecoder878 mannequin opened this issue Oct 4, 2021 · 4 comments
Labels
3.9 only security fixes topic-ctypes type-bug An unexpected behavior, bug, or error

Comments

@simplecoder878
Copy link
Mannequin

simplecoder878 mannequin commented Oct 4, 2021

BPO 45366
Nosy @ericvsmith, @sobolevn, @akulakov

Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

Show more details

GitHub fields:

assignee = None
closed_at = None
created_at = <Date 2021-10-04.20:17:33.399>
labels = ['ctypes', 'type-bug', '3.9']
title = 'dataclass init=False field with default works but default_factory does not'
updated_at = <Date 2021-11-08.01:28:02.575>
user = 'https://bugs.python.org/simplecoder878'

bugs.python.org fields:

activity = <Date 2021-11-08.01:28:02.575>
actor = 'andrei.avk'
assignee = 'none'
closed = False
closed_date = None
closer = None
components = ['ctypes']
creation = <Date 2021-10-04.20:17:33.399>
creator = 'simple_coder878'
dependencies = []
files = []
hgrepos = []
issue_num = 45366
keywords = []
message_count = 4.0
messages = ['403182', '403183', '403206', '405920']
nosy_count = 4.0
nosy_names = ['eric.smith', 'sobolevn', 'andrei.avk', 'simple_coder878']
pr_nums = []
priority = 'normal'
resolution = None
stage = None
status = 'open'
superseder = None
type = 'behavior'
url = 'https://bugs.python.org/issue45366'
versions = ['Python 3.9']

@simplecoder878
Copy link
Mannequin Author

simplecoder878 mannequin commented Oct 4, 2021

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.

@simplecoder878 simplecoder878 mannequin added 3.9 only security fixes topic-ctypes type-bug An unexpected behavior, bug, or error labels Oct 4, 2021
@simplecoder878
Copy link
Mannequin Author

simplecoder878 mannequin commented Oct 4, 2021

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

@sobolevn
Copy link
Member

sobolevn commented Oct 5, 2021

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_factoriess when object is created.

@akulakov
Copy link
Contributor

akulakov commented Nov 8, 2021

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".

@ezio-melotti ezio-melotti transferred this issue from another repository Apr 10, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3.9 only security fixes topic-ctypes type-bug An unexpected behavior, bug, or error
Projects
None yet
Development

No branches or pull requests

2 participants