I often find myself wanting to do someting like this:
from abc import abstractmethod
from dataclasses import dataclass
class HasLength:
@property
@abstractmethod
def len(self) -> int: ...
def __len__(self) -> int:
return self.len
@dataclass
class MyClass(HasLength):
len: int
m = MyClass(3) # AttributeError: can't set attribute 'len'
len(m)
but trying to override a property in a base class with a dataclass field causes an AttributeError
.
I suspect this has something to do with the fact that properties can also have a setter, because normal methods can be overridden just fine at runtime.
Would it be possible to allow this kind of overriding? From a typing perspective, there is no problem with this because a field has strictly more functionality than a (read-only) property, so this doesn’t break the Liskov substitution principle.
Here are some alternatives that I’ve considered:
Don’t declare the property:
from dataclasses import dataclass
class HasLength:
def __len__(self) -> int:
return self.len
@dataclass
class MyClass(HasLength):
len: int
m = MyClass(3)
len(m)
This works fine at runtime but I’m losing the ability for static type checking.
Using implicit structural typing with Protocol
:
from abc import abstractmethod
from dataclasses import dataclass
from typing import Protocol
class HasLength(Protocol):
@property
@abstractmethod
def len(self) -> int: ...
@dataclass
class MyClass:
len: int
def get_length(x: HasLength) -> int:
return x.len
m: HasLength = MyClass(3)
get_length(m)
This works at runtime and it type-checks but I lose the ability to define methods in the base class.
Guarding the definition of the abstract property with if TYPE_CHECKING
:
from abc import abstractmethod
from dataclasses import dataclass
from typing import TYPE_CHECKING
class HasLength:
if TYPE_CHECKING: # always `False` at runtime
@property
@abstractmethod
def len(self) -> int: ...
def __len__(self) -> int:
return self.len
@dataclass
class MyClass(HasLength):
len: int
m = MyClass(3)
len(m)
This of course works at runtime and mypy also happens to not complain about it, but it’s a bit weird.