Allow overriding (abstract) properties with fields

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.

2 Likes

I think you should consider changing your design. From just what you’ve shown, you might just remove the base class len, and have the derived dataclass implement __len__ by delegating to the dataclass field.

In general though, I try to avoid overriding properties. Once you have the complexity of inheritance, it seems counterintuitive to treat a method like a property.

If len being an abstract property isn’t important to you, you can just inherit from the protocol:

from dataclasses import dataclass
from typing import Protocol

class HasLength(Protocol):
    len: int

    def __len__(self) -> int:
        return self.len

@dataclass
class MyClass(HasLength):
    len: int

def get_length(x: HasLength) -> int:
    return x.len

m: HasLength = MyClass(3)
get_length(m)

Works at runtime and omitting len in MyClass will be caught by your type checker, because protocol attributes are considered to be abstract if they are not initialised.

The ability to override abstract properties with dataclass would be much desired.

To address this, I created a Python library, dataclass-abc, which provides support for implementing abstract properties within dataclasses.