I am trying to make mypy happy with my type annotations. Here is minimal example:
class FooInterface:
x: int
class FooWithAttribute(FooInterface):
x: int = 0
class FooWithProperty(FooInterface):
@property
def x(self) -> int:
return 0
To my human understanding everything is fine: both FooWithAttribute().x
and FooWithProperty().x
will return 0
which is int
, no type errors. However mypy complains:
error: Signature of "x" incompatible with supertype "FooInterface"
Is there a way to tell mypy that everything is OK? Right now the only way I found is annotating x: typing.Any
in FooInterface
which wastes the information that x is int.
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:
FooInterface.x
also be a read-only propertyFooWithProperty.x
to make it writableI'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.