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.

1 Like

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.