I'm tired of people wrongly implement some class, not according to the base class interface, like adding new positional argument not defined in the base class method. This usually happened when you have plugin pattern where multiple backend or implementation exists for the given functionality. And each backend was developed by separate team/developer.
Or having to write tests just to make sure correct parameters being passed to function. Tests should be written to verify the logic, not the language syntax or semantic.
The first thing I want is something that can verify method signature is according to the base class specification.
Let see the following simple example:-
class A():
def __init__(self, x):
self.x = x
def send(self, msg):
pass
class B(A):
def send(self, msg):
return True
The problem with above code is that it possible for other developer (or team) to implement send()
method in B
class in a way that incompatible with other A subclass. For example by adding extra parameter:-
class A():
def __init__(self, x):
self.x = x
def send(self, msg):
pass
class B(A):
def send(self, msg, extra):
return True
This is possible to go unnoticed until you need to plugin another subclass of A
to replace B
but that implementation seem not compatible as it doesn't accept the extra
parameter. This can be avoided if we have a way to type check the method signature to make sure it conform to the base class specification.
import abc
class A(abc.ABC):
def __init__(self, x: int) -> None:
self.x = x # Attribute x of type int
@abc.abstractmethod
def send(self, msg: str) -> bool:
pass
class B(A):
def send(self, msg: str) -> bool:
return True
b = B(1)
b.x = 2 # OK
b.send('hello')
Now if someone try to implement B
by adding additional parameter to send()
, such as:-
import abc
class A(abc.ABC):
def __init__(self, x: int) -> None:
self.x = x # Attribute x of type int
@abc.abstractmethod
def send(self, msg: str) -> bool:
pass
class B(A):
def send(self, msg: str, extra: str) -> bool:
return True
b = B(1)
b.x = 2 # OK
b.send('hello', 'extra')
Running that through mypy will give us error:-
base.py:12: error: Signature of "send" incompatible with supertype "A"
Nice, now we solved problem mentioned at the beginning of this article.
Another common issue is mismatch in the return value. For example:-
import abc
class A(abc.ABC):
def __init__(self, x: int) -> None:
self.x = x # Attribute x of type int
@abc.abstractmethod
def send(self, msg: str) -> bool:
pass
class B(A):
def send(self, msg: str) -> bool:
return 'hello'
b = B(1)
b.x = 2 # OK
b.send('hello')
Above, method send()
in class A was annotated to return a Boolean but since B.send()
return a string, mypy would complain:-
base.py:13: error: Incompatible return value type (got "str", expected "bool")
I can see some potential in mypy here. It can be a good compliment to our tests suite so we don't have to write tests for some cases that can actually being caught by compiler (if we do have one). But unfortunately, mypy does not yet support namespaced package and we do use namespaced package a lot.
Another issue we faced was with buildout. To provide type hinting for standard library (which most doesn't have the annotation yet), mypy provide a shadowing library called typeshed. Buildout script define it's own sys.path
but mypy is ignoring sys.path
because it need to look in the typeshed first, this mean all the paths set by buildout in our script not usable. We have to specify that as MYPYPATH
but that still didn't work so I just gave up.