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(slots=True) does not account for slots in base classes
Type: behavior Stage: resolved
Components: Library (Lib) Versions: Python 3.11, Python 3.10
process
Status: closed Resolution: fixed
Dependencies: Superseder:
Assigned To: eric.smith Nosy List: AlexWaygood, Spencer Brown, ariebovenberg, eric.smith, hynek, serhiy.storchaka, sobolevn
Priority: normal Keywords: patch

Created on 2022-01-14 19:31 by ariebovenberg, last changed 2022-04-11 14:59 by admin. This issue is now closed.

Pull Requests
URL Status Linked Edit
PR 31980 merged ariebovenberg, 2022-03-18 16:34
Messages (19)
msg410591 - (view) Author: Arie Bovenberg (ariebovenberg) * Date: 2022-01-14 19:31
@dataclass(slots=True) adds slots to dataclasses. It adds a slot per field. 
However, it doesn't account for slots already present in base classes:

>>> class Base:
...     __slots__ = ('a', )
...
>>> @dataclass(slots=True)
... class Foo(Base):
...     a: int
...     b: float
...
>>> Foo.__slots__
('a', 'b')  # should be: ('b', )


The __slots__ documentation says:

    If a class defines a slot also defined in a base class, the instance variable 
    defined by the base class slot is inaccessible (except by retrieving its descriptor 
    directly from the base class). This renders the meaning of the program undefined. 
    In the future, a check may be added to prevent this.

Solution: don't add slots which are already defined in any base classes:

>>> @dataclass
... class Bla(Base):
...     __slots__ = ('b', )
...     a: int
...     b: float
...
>>> Bla(4, 5.65)
Bla(a=4, b=5.65)

If you agree, I'd like to submit a PR to fix this. I already have a prototype working.
msg410601 - (view) Author: Eric V. Smith (eric.smith) * (Python committer) Date: 2022-01-14 21:32
I'll have to do some more research. But your analysis looks correct to me, so far.
msg410628 - (view) Author: Arie Bovenberg (ariebovenberg) * Date: 2022-01-15 07:33
There are already 2 complexities I can think of:

1. This behavior may break some people's code, if they use __slots__ to iterate over
   the fields of a dataclass. Solution: explicitly mention in the docs that
   not every field may get a slot on the new class. Advise them to use
   `fields()` to iterate over the fields.
2. It's technically allowed for __slots__ to be an iterator (which will then be 
   exhausted at class creation). Finding the __slots__ of such a class
   may require more elaborate introspection.
msg410630 - (view) Author: Nikita Sobolev (sobolevn) * (Python triager) Date: 2022-01-15 08:23
Arie, can you please explain what is the technical difference between these two cases:

```python
class A:
    __slots__ = ('a', )
    # fields

class B(A):
    __slots__ = ('a', 'b')
    # fields
```

And:

```python
class C:
    __slots__ = ('a', )
    # fields

class D(C):
    __slots__ = ('b', )
    # fields
```

?
msg410641 - (view) Author: Spencer Brown (Spencer Brown) * Date: 2022-01-15 11:05
Both will function, but class B will add its slots after A's, causing there to be an extra unused slot in the object that you can only access by directly using the A.a descriptor. So all slotted inheriting dataclasses cause the object to use more memory than necessary.
msg410642 - (view) Author: Arie Bovenberg (ariebovenberg) * Date: 2022-01-15 11:58
Spencer is correct.

The documentation even adds: "This renders the meaning of the program undefined."

It's clear it doesn't break anything users would often encounter (we would have heard about it), but it's still undefined behavior.
msg410820 - (view) Author: Eric V. Smith (eric.smith) * (Python committer) Date: 2022-01-17 20:13
It would also be interesting to see what attrs does in this case.
msg410841 - (view) Author: Hynek Schlawack (hynek) * (Python committer) Date: 2022-01-18 06:11
>>> @attrs.define
... class C(Base):
...   a: int
...   b: int
...
>>> C.__slots__
('b', '__weakref__')

We've got a test specifically for this use case: https://github.com/python-attrs/attrs/blob/5f36ba9b89d4d196f80147d4f2961fb2f97ae2e5/tests/test_slots.py#L309-L334
msg411010 - (view) Author: Arie Bovenberg (ariebovenberg) * Date: 2022-01-20 08:05
@hynek interesting! 

The discussion in https://github.com/python-attrs/attrs/pull/420 on the weakref slot is very interesting as well.

Considering __weakref__ is something we don't want to make impossible in dataclasses, @eric.smith what would be your preferred solution?
msg415393 - (view) Author: Arie Bovenberg (ariebovenberg) * Date: 2022-03-17 08:05
@eric.smith did you give this some thought? Would we want to imitate the attrs behavior regarding __weafref__?

It'd be nice if I can submit a PR to be included in 3.11
msg415399 - (view) Author: Serhiy Storchaka (serhiy.storchaka) * (Python committer) Date: 2022-03-17 11:36
__slotnames__ is used for storing all slot names.

New object.__getstate__() proposed in issue26579 may have some relation to this discussion.
msg415400 - (view) Author: Eric V. Smith (eric.smith) * (Python committer) Date: 2022-03-17 11:53
Serhiy: Could you point to some documentation on __slotnames__? I see a few references in the code to it, but it's not set on simple test class.

>>> class A:
...     __slots__=('a',)
...
>>> A.__slotnames__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: type object 'A' has no attribute '__slotnames__'. Did you mean: '__slots__'?
>>>
msg415401 - (view) Author: Serhiy Storchaka (serhiy.storchaka) * (Python committer) Date: 2022-03-17 12:29
>
> It is not set automatically. It is set in object.__reduce__ by calling
> some helper function from the copyreg module.
msg415419 - (view) Author: Serhiy Storchaka (serhiy.storchaka) * (Python committer) Date: 2022-03-17 15:50
Sorry, the previous message was written via a phone because of blackout, so the formatting is a tine bit messy.

__slotnames__ is set in copyreg._slotnames(). If you want to keep a list of all slots you should save it in __slotnames__, either using copyreg._slotnames() directly or by duplicating its code.

But as for the original issue, I do not think that the current behavior is incorrect. At least it is consistent with the behavior of regular classes.
msg415453 - (view) Author: Eric V. Smith (eric.smith) * (Python committer) Date: 2022-03-17 22:12
I don't have a problem saying that for a class to be used as a base class for a dataclass, its __slots__ must not be an iterator that's been exhausted. That doesn't seem like a very onerous requirement.

I'm also not concerned about people using __slots__ to iterate over the fields, but I agree that a documentation note couldn't hurt.

@ariebovenberg: go ahead and submit your PR. Thanks!
msg415471 - (view) Author: Arie Bovenberg (ariebovenberg) * Date: 2022-03-18 06:31
@eric.smith awesome! 

What would you like to do about the __weakref__ slot?

1. take no action
2. create a separate ticket to discuss further
3. implement as done in attrs, now that we're changing __slots__ dataclass behavior anyway

I wouldn't recommend (1), because sooner or later this feature request will pop up. It seems reasonable to support it.
msg415474 - (view) Author: Eric V. Smith (eric.smith) * (Python committer) Date: 2022-03-18 09:07
I thought there was an existing issue that covered this, but now I can't find it.

I'd prefer #2, create a separate issue.
msg415570 - (view) Author: Eric V. Smith (eric.smith) * (Python committer) Date: 2022-03-19 21:01
New changeset 82e9b0bb0ac44d4942b9e01b2cdd2ca85c17e563 by Arie Bovenberg in branch 'main':
bpo-46382 dataclass(slots=True) now takes inherited slots into account (GH-31980)
https://github.com/python/cpython/commit/82e9b0bb0ac44d4942b9e01b2cdd2ca85c17e563
msg415571 - (view) Author: Eric V. Smith (eric.smith) * (Python committer) Date: 2022-03-19 21:01
Thanks for all of your work, @ariebovenberg!
History
Date User Action Args
2022-04-11 14:59:54adminsetgithub: 90540
2022-03-19 21:01:51eric.smithsetstatus: open -> closed
resolution: fixed
messages: + msg415571

stage: patch review -> resolved
2022-03-19 21:01:23eric.smithsetmessages: + msg415570
2022-03-18 16:34:59ariebovenbergsetkeywords: + patch
stage: patch review
pull_requests: + pull_request30070
2022-03-18 09:07:29eric.smithsetmessages: + msg415474
2022-03-18 06:31:52ariebovenbergsetmessages: + msg415471
2022-03-17 22:12:19eric.smithsetmessages: + msg415453
2022-03-17 15:50:08serhiy.storchakasetmessages: + msg415419
2022-03-17 12:29:11serhiy.storchakasetmessages: + msg415401
2022-03-17 11:53:54eric.smithsetmessages: + msg415400
2022-03-17 11:36:02serhiy.storchakasetnosy: + serhiy.storchaka
messages: + msg415399
2022-03-17 08:05:27ariebovenbergsetmessages: + msg415393
2022-01-20 08:05:06ariebovenbergsetmessages: + msg411010
2022-01-18 06:11:28hyneksetmessages: + msg410841
2022-01-17 22:04:25AlexWaygoodsetnosy: + hynek
2022-01-17 20:13:11eric.smithsetmessages: + msg410820
2022-01-15 11:58:23ariebovenbergsetmessages: + msg410642
2022-01-15 11:13:08AlexWaygoodsetnosy: + AlexWaygood
2022-01-15 11:05:34Spencer Brownsetnosy: + Spencer Brown
messages: + msg410641
2022-01-15 08:23:19sobolevnsetnosy: + sobolevn
messages: + msg410630
2022-01-15 07:33:17ariebovenbergsetmessages: + msg410628
2022-01-14 21:32:28eric.smithsetassignee: eric.smith

messages: + msg410601
nosy: + eric.smith
2022-01-14 19:31:43ariebovenbergcreate