Override Python Properties With Simple Attributes
Both Mypy and Pyright raise errors when you try to override a property declared in an interface with a simple attribute.
from abc import ABC, abstractmethod
class Base(ABC):
@property
@abstractmethod
def value(self) -> int: ...
class Sub(Base):
def __init__(self, value: int):
self.value = value
# pyright
# error: Cannot assign to attribute "value" for class "Sub*"
# "int" is not assignable to "property" (reportAttributeAccessIssue)
# mypy
# error: Property "value" defined in "Base" is read-only [misc]
Mypy interprets value
as being strictly read-only. Pyright considers properties and simple attributes to be semantically different.
An alternative approach might be overriding a simple attribute in the interface with a computed property in the concrete implementation. However, this causes issues with Mypy and Pyright.
class Base:
value: int
class Sub(Base):
@property
def value(self) -> int:
return 42
# pyright
# error: "value" overrides symbol of same name in class "Base"
# "property" is not assignable to "int" (reportIncompatibleVariableOverride)
# mypy
# error: Cannot override writeable attribute with read-only property [override]
Both Pyright and Mypy raise fair issues, but there still might be cases where you want to ensure read access to some attribute exists and don’t care whether it’s a i) computed property or simple attribute, or ii) whether is read-only or read-write.
One way to override simple attributes with a computed property is wrapping property
with a return type annotation matching the decorated function.
from typing import Callable, Any
def typed_property[T](func: Callable[[Any], T]) -> T:
return property(fget=func) # type: ignore
class NewBase:
value: int
class AttrSub(NewBase):
def __init__(self, value: int):
self.value = value
class ComputedSub(NewBase):
@typed_property
def value(self) -> int:
return 42
# pyright: 0 errors, 0 warnings, 0 informations
# mypy: Success: no issues found in 1 source file
This approach allows subclasses to override value
using either a simple attribute or a computed property. Note that since typed_property
only sets a getter method, trying to either set or delete ComputedSub().value
will cause a runtime error.
The revised approach below allows users to assign a new value to value
. Assignment will set key-value pair in the instance’s __dict__
which will be retrieved when value
is accessed. Data descriptors on the class take precedence over __dict__
values in Python’s attribute lookup process, but non-data descriptors come after __dict__
lookups. Since the built-in property
type is a data descriptor, the implementation below defines a new property type (that is a non-data descriptor).
class TypedProperty:
def __init__(self, func):
self.func = func
def __get__(self, obj, objtype=None):
if obj is None:
return self
return self.func(obj)
def typed_property[T](func: Callable[[Any], T]) -> T:
return TypedProperty(func) # type: ignore
class ComputedSub(NewBase):
@typed_property
def value(self) -> int:
return 42
cs = ComputedSub()
cs.value
# 42
cs.value = 12
cs.__dict__
# {'value': 12}
cs.value
# 12
del cs.value
cs.value
# 42
# pyright: 0 errors, 0 warnings, 0 informations
# mypy: Success: no issues found in 1 source file
Note that the setter will not work for classes with __slots__
due to how they retrieve attributes.