Mypy is actually pointing out a legitimate bug in your program. To demonstrate, suppose you have a program that looks like this:
def mutate(f: FooInterface) -> None:
f.x = 100
Seems fine, right? But what happens if we do mutate(FooWithProperty())
? Python will actually crash with an AttributeError
!
Traceback (most recent call last):
File "test.py", line 19, in <module>
mutate(FooWithProperty())
File "test.py", line 16, in mutate
f.x = 100
AttributeError: can't set attribute
To make mypy happy, you basically have two options:
- Make
FooInterface.x
also be a read-only property
- Implement a setter for
FooWithProperty.x
to make it writable
I'm guessing that in your case, you probably want to take approach 1. If you do so, mypy will correctly point out that the line f.x = 100
is not permitted:
from abc import abstractmethod
class FooInterface:
# Marking this property as abstract is *optional*. If you do it,
# mypy will complain if you forget to define x in a subclass.
@property
@abstractmethod
def x(self) -> int: ...
class FooWithAttribute(FooInterface):
# No complaints from mypy here: having this attribute be writable
# won't violate the Liskov substitution principle -- it's safe to
# use FooWithAttribute in any location that expects a FooInterface.
x: int = 0
class FooWithProperty(FooInterface):
@property
def x(self) -> int:
return 0
def mutate(f: FooInterface) -> None:
# error: Property "x" defined in "FooInterface" is read-only
f.x = 100
mutate(FooWithProperty())
Approach 2 unfortunately doesn't quite work yet due to a bug in mypy -- mypy doesn't correctly understand how to handle overriding an attribute with a property. The workaround in this case is to make FooInterface.x
a property with a setter.