Do not include any members in __slots__ that are already in a base class's __slots__.
.. versionadded:: 3.10
+ .. versionchanged:: 3.11
+ If a field name is already included in the ``__slots__``
+ of a base class, it will not be included in the generated ``__slots__``
+ to prevent `overriding them <https://docs.python.org/3/reference/datamodel.html#notes-on-using-slots>`_.
+ Therefore, do not use ``__slots__`` to retrieve the field names of a
+ dataclass. Use :func:`fields` instead.
+ To be able to determine inherited slots,
+ base class ``__slots__`` may be any iterable, but *not* an iterator.
+
+
``field``\s may optionally specify a default value, using normal
Python syntax::
import keyword
import builtins
import functools
+import itertools
import abc
import _thread
from types import FunctionType, GenericAlias
object.__setattr__(self, field.name, value)
+def _get_slots(cls):
+ match cls.__dict__.get('__slots__'):
+ case None:
+ return
+ case str(slot):
+ yield slot
+ # Slots may be any iterable, but we cannot handle an iterator
+ # because it will already be (partially) consumed.
+ case iterable if not hasattr(iterable, '__next__'):
+ yield from iterable
+ case _:
+ raise TypeError(f"Slots of '{cls.__name__}' cannot be determined")
+
+
def _add_slots(cls, is_frozen):
# Need to create a new class, since we can't set __slots__
# after a class has been created.
# Create a new dict for our new class.
cls_dict = dict(cls.__dict__)
field_names = tuple(f.name for f in fields(cls))
- cls_dict['__slots__'] = field_names
+ # Make sure slots don't overlap with those in base classes.
+ inherited_slots = set(
+ itertools.chain.from_iterable(map(_get_slots, cls.__mro__[1:-1]))
+ )
+ cls_dict["__slots__"] = tuple(
+ itertools.filterfalse(inherited_slots.__contains__, field_names)
+ )
for field_name in field_names:
# Remove our attributes, if present. They'll still be
# available in _MARKER.
x: int
def test_generated_slots_value(self):
- @dataclass(slots=True)
- class Base:
- x: int
- self.assertEqual(Base.__slots__, ('x',))
+ class Root:
+ __slots__ = {'x'}
+
+ class Root2(Root):
+ __slots__ = {'k': '...', 'j': ''}
+
+ class Root3(Root2):
+ __slots__ = ['h']
+
+ class Root4(Root3):
+ __slots__ = 'aa'
@dataclass(slots=True)
- class Delivered(Base):
+ class Base(Root4):
y: int
+ j: str
+ h: str
+
+ self.assertEqual(Base.__slots__, ('y', ))
+
+ @dataclass(slots=True)
+ class Derived(Base):
+ aa: float
+ x: str
+ z: int
+ k: str
+ h: str
- self.assertEqual(Delivered.__slots__, ('x', 'y'))
+ self.assertEqual(Derived.__slots__, ('z', ))
@dataclass
- class AnotherDelivered(Base):
+ class AnotherDerived(Base):
z: int
- self.assertTrue('__slots__' not in AnotherDelivered.__dict__)
+ self.assertNotIn('__slots__', AnotherDerived.__dict__)
+
+ def test_cant_inherit_from_iterator_slots(self):
+
+ class Root:
+ __slots__ = iter(['a'])
+
+ class Root2(Root):
+ __slots__ = ('b', )
+
+ with self.assertRaisesRegex(
+ TypeError,
+ "^Slots of 'Root' cannot be determined"
+ ):
+ @dataclass(slots=True)
+ class C(Root2):
+ x: int
def test_returns_new_class(self):
class A:
--- /dev/null
+:func:`~dataclasses.dataclass` ``slots=True`` now correctly omits slots already
+defined in base classes. Patch by Arie Bovenberg.