Search code examples
pythoninheritancepython-dataclasses

Python @dataclasses(slots=True) breaks super()


Consider the following code. I have a base and derived class, both dataclasses, and I want to call a method of the base class in the derived class via super():

import abc
import dataclasses
import typing


SLOTS = False


@dataclasses.dataclass(slots=SLOTS)
class Base:
    @abc.abstractmethod
    def f(self, x: int) -> int:
        return x


@dataclasses.dataclass(slots=SLOTS)
class Derived(Base):
    @typing.override
    def f(self, x: int) -> int:
        return super().f(x)


d = Derived()
d.f(2)

When setting SLOTS = False, this runs fine. When setting SLOTS = True, I get an error:

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[2], line 24
     20         return super().f(x)
     23 d = Derived()
---> 24 d.f(2)

Cell In[2], line 20, in Derived.f(self, x)
     18 @typing.override
     19 def f(self, x: int) -> int:
---> 20     return super().f(x)

TypeError: super(type, obj): obj (instance of Derived) is not an instance or subtype of type (Derived).

Why is that?

Note that it works if I use a regular slotted class instead of a dataclass:

import abc
import typing


class Base:
    __slots__ = tuple()

    @abc.abstractmethod
    def f(self, x: int) -> int:
        return x


class Derived(Base):
    __slots__ = tuple()

    @typing.override
    def f(self, x: int) -> int:
        return super().f(x)


d = Derived()
d.f(2)

Solution

  • WHen one uses the slots=True option on dataclasses, it will create a new class using the namespace of the decorated class - while not passing it makes it modify the class "in place". (This is needed because using slots really change how the class is built - its "layout" as it is called)

    The simplest thing to do is not to use __slots__ altogether - the gains slots gives one in modern Python are small since some of the optimizations that went on Python 3.11 (check it here: Are Python 3.11 objects as light as slots? )

    Anyway, the workaround is quite simple - instead of the parameterless version of super(), which uses a mechanism introduced in Python 3.0, just use the explicit version of it, where you pass the class and instance as arguments.

    The thing is that the parameterless version uses implicitly the class body where the super() call is actually written in: at compile time, the current class is frozen into the method, in a non-local like variable named __class__. The new class created by dataclass with slots can't (or simply does not do) update this value - so parameterless super will try to call super on the original (pre dataclass decorator ) Derived class and will fail.

    This version of the code will work:

    
    @dataclasses.dataclass(slots=SLOTS)
    class Derived(Base):
        @typing.override
        def f(self, x: int) -> int:
            return super(Derived, self).f(x)