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)
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.
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.