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

Question:

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)

Asked By: Roy Smart

||

Answers:

So, the thing is, you declared an abstract property. Not an abstract constructor argument, or an abstract instance dict entryabc 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.

  • Your Child1 doesn’t create a class dict entry for inherited_attribute – the annotation only creates an entry in the annotation dict.
  • Child2 does create an entry in the class dict, but then the dataclass machinery removes it, because it’s a field with no default value. This changes the abstractness status of Child2, which is undefined behavior below Python 3.10, but Python 3.10 added abc.update_abstractmethods to support that, and dataclasses uses that function on Python 3.10.
  • Child3 creates an entry in the class dict, and since the dataclass machinery sees this entry as a default value, it leaves the entry there, so the abstract property is considered overridden.

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.

Answered By: user2357112

Answering my own question since I just found another option than those listed in @user2357112’s excellent answer.

What seems to work is setting the default value of the field to dataclasses.MISSING like in the following example:

@dataclasses.dataclass
class Child4(Parent):
    inherited_attribute: int = dataclasses.MISSING

This might be better than @user2357112’s Option 3 since it actually raises a TypeError: Child4.__init__() missing 1 required positional argument: 'inherited_attribute' if the value of inherited_attribute is missing, instead of silently setting it to the property Parent.inherited_attribute.

This is probably more of a hack than a real solution since the documentation of dataclasses.field() says that "No code should directly use the MISSING value."

Answered By: Roy Smart