python / cpython

The Python programming language
https://www.python.org
Other
62.33k stars 29.94k forks source link

dataclass with slots=True, init=False raises AttributeError #98247

Open noskill opened 1 year ago

noskill commented 1 year ago

Python 3.10.4 dataclass with slots=True, init=False raises AttributeError on attribute access

from dataclasses import dataclass

@dataclass(slots=True, init=False)
class WorldState:
    is_mission_running: bool = False

w = WorldState()
w.is_mission_running

Traceback (most recent call last): File "", line 1, in AttributeError: 'WorldState' object has no attribute 'is_mission_running'

OOFLORDISME commented 1 year ago

A tip is to declare your variables at the beginning of your code that way someone else knows where they are and how to debug the code.

ericvsmith commented 1 year ago

Interesting. By specifiying init=False, you're taking on responsibility for what dataclass's supplied __init__ does. One of the things it does is assign to all of the instance variables when slots=True. I think it's on you to do that in this case. I don't see how dataclasses can help here, but I'm open to discussion about it.

noskill commented 1 year ago

yeah, it makes sense, but now i am confused why the same code works with slots=False, init=False

>>> @dataclass(slots=False, init=False)
... class WorldState:
...     is_mission_running: bool = True
... 
>>> w = WorldState()
>>> w.is_mission_running
True
ericvsmith commented 1 year ago

dataclasses is basically doing this:

class WorldState:
    is_mission_running: bool = False

cls = type(WorldState)("WorldState", (), {'__slots__': "is_mission_running"})
WorldState = cls

After which:

w = WorldState()
w.is_mission_running

gives the error you see. It's because there's no class attribute to fall back to.

Now that I think about it, I guess dataclasses could add cls.is_mission_running = WorldState.is_mission_running after it creates the new class. For reasons I haven't thought through, you can't just add is_mission_running to the class dict (the 3rd argument to type(WorldState)()). That gives ValueError: 'is_mission_running' in __slots__ conflicts with class variable.

ericvsmith commented 1 year ago

yeah, it makes sense, but now i am confused why the same code works with slots=False, init=False

In case it's not clear: it's because with slots=False, x.is_mission_running refers to WorldState.is_mission_running. This attribute exists and is False. But with slots=True, the newly created WorldState class doesn't have this attribute. The more I think about it, the more I think the right thing to do is to copy any field's default value from the old class to the new class, and stop doing the instance assignment of default values in the generated __init__. But this change is probably user visible, so maybe I shouldn't backport it to 3.10 and 3.11. I'm still mulling it over.

noskill commented 1 year ago

Thank you. I am closing the issue since the behaviour is consistent with what you get with defining __slots__ manually and not defining __init__

>>> class WorldState:
...     __slots__ = ['is_mission_running']
... 
>>> w = WorldState()
>>> w.is_mission_running
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'WorldState' object has no attribute 'is_mission_running'
ericvsmith commented 1 year ago

I'm going to leave it open, if that's okay. I'm leaning towards changing dataclass's behavior here to match the experience if you don't use slots.

nstarman commented 1 year ago

We've run into this issue at Astropy, where we're trying to make some objects lighter-weight using slots, but can't because they are dataclasses.