pythonabstract-classpython-dataclassesrequired-field

Is there a way to make an inherited abstract property a required constructor argument in a Python dataclass?


I'm using Python dataclasses with inheritance and I would like to make an inherited abstract property into a required constructor argument. Using an inherited abstract property as a optional constructor argument works as expected, but I've been having real trouble making the argument required.

Below is a minimal working example, test_1() fails with TypeError: Can't instantiate abstract class Child1 with abstract methods inherited_attribute, test_2() fails with AttributeError: can't set attribute, and test_3() works as promised.

Does anyone know a way I can achieve this behavior while still using dataclasses?

import abc
import dataclasses

@dataclasses.dataclass
class Parent(abc.ABC):

    @property
    @abc.abstractmethod
    def inherited_attribute(self) -> int:
        pass

@dataclasses.dataclass
class Child1(Parent):
    inherited_attribute: int

@dataclasses.dataclass
class Child2(Parent):
    inherited_attribute: int = dataclasses.field()

@dataclasses.dataclass
class Child3(Parent):
    inherited_attribute: int = None

def test_1():
    Child1(42)

def test_2():
    Child2(42)

def test_3():
    Child3(42)


Solution

  • So, the thing is, you declared an abstract property. Not an abstract constructor argument, or an abstract instance dict entry - abc has no way to specify such things.

    Abstract properties are really supposed to be overridden by concrete properties, but the abc machinery will consider it overridden if there is a non-abstract entry in the subclass's class dict.


    So you've got a few courses of action here. The first is to remove the abstract property. You don't want to force your subclasses to have a property - you want your subclasses to have an accessible inherited_attribute instance attribute, and it's totally fine if this attribute is implemented as an instance dict entry. abc doesn't support that, and using an abstract property is wrong, so just document the requirement instead of trying to use abc to enforce it.

    With the abstract property removed, Parent isn't actually abstract any more, and in fact doesn't really do anything, so at that point, you can just take Parent out entirely.


    Option 2, if you really want to stick with the abstract property, would be to give your subclasses a concrete property, properly overriding the abstract property:

    @dataclasses.dataclass
    class Child(Parent):
        _hidden_field: int
        @property
        def inherited_attribute(self):
            return self._hidden_field
    

    This would require you to give the field a different name from the attribute name you wanted, with consequences for the constructor argument names, the repr output, and anything else that cares about field names.


    The third option is to get something else into the class dict to shadow the inherited_attribute name, in a way that doesn't get treated as a default value. Python 3.10 added slots support in dataclasses, so you could do

    @dataclasses.dataclass(slots=True)
    class Child(Parent):
        inherited_attribute: int
    

    and the generated slot descriptor would shadow the abstract property, without being treated as a default value. However, this would not give the usual memory savings of slots, because your classes inherit from Parent, which doesn't use slots.


    Overall, I would recommend option 1. Abstract properties don't mean what you want, so just don't use them.