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 -
abchas no way to specify such things.Abstract properties are really supposed to be overridden by concrete properties, but the
abcmachinery will consider it overridden if there is a non-abstract entry in the subclass's class dict.Child1doesn't create a class dict entry forinherited_attribute- the annotation only creates an entry in the annotation dict.Child2does create an entry in the class dict, but then the dataclass machinery removes it, because it's afieldwith no default value. This changes the abstractness status ofChild2, which is undefined behavior below Python 3.10, but Python 3.10 addedabc.update_abstractmethodsto support that, anddataclassesuses that function on Python 3.10.Child3creates 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_attributeinstance attribute, and it's totally fine if this attribute is implemented as an instance dict entry.abcdoesn't support that, and using an abstract property is wrong, so just document the requirement instead of trying to useabcto enforce it.With the abstract property removed,
Parentisn't actually abstract any more, and in fact doesn't really do anything, so at that point, you can just takeParentout 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:
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
reproutput, and anything else that cares about field names.The third option is to get something else into the class dict to shadow the
inherited_attributename, in a way that doesn't get treated as a default value. Python 3.10 added slots support indataclasses, so you could doand 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.