mypy: A Quick Look into Static Type Checking in Python

Kamal Mustafa - May 1 '18 - - Dev Community

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.

https://medium.com/@ageitgey/learn-how-to-use-static-type-checking-in-python-3-6-in-10-minutes-12c86d72677b

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

Running that through mypy will give us error:-

base.py:12: error: Signature of "send" incompatible with supertype "A"
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .