Check type annotations in Python with mypy

Check type annotations in Python with mypy

Learn more about solving common Python problems in our series covering seven PyPI libraries.

Image credits : 

Lewis Cowles, CC BY-SA 4.0

x

Get the newsletter

Join the 85,000 open source advocates who receive our giveaway alerts and article roundups.

Python is one of the most popular programming languages in use today—and for good reasons: it's open source, it has a wide range of uses (such as web programming, business applications, games, scientific programming, and much more), and it has a vibrant and dedicated community supporting it. This community is the reason we have such a large, diverse range of software packages available in the Python Package Index (PyPI) to extend and improve Python and solve the inevitable glitches that crop up.

In this series, we've looked at seven PyPI libraries that can help you solve common Python problems. Today, in the final article, we'll look at mypy "a Python linter on steroids."

mypy

Python is a "dynamically typed" language. However, sometimes it is nice to let other beings, both robotic and human, know what types are expected. Traditionally, humans have been prioritized: input and output types of functions were described in docstrings. MyPy allows you to put the robots on equal footing, letting them know what types are intended.

Let's look at the following code:

def add_one(input):
    return input + 1

def print_seven():
    five = "5"
    seven = add_one(add_one(five))
    print(seven)

Calling print_seven raises a TypeError informing us we cannot add a string and a number: we cannot add "5" and 1.

However, we cannot know this until we run the code. Running the code, if it were correct, would have produced a printout to the screen: a side-effect. A relatively harmless one, as side-effects go, but still, a side-effect. Is it possible to do it without risking any side-effects?

We just have to let the robots know what to expect.

def add_one(input: int) -> int:
    return input + 1

def print_seven() -> None:
    five = "5"
    seven = add_one(add_one(five))
    print(seven)

We use type annotations to denote that add_one expects an integer and returns an integer. This does not change what the code does. However, now we can ask a safe robot to find problems for us.

$ mypy typed.py
typed.py:6: error: Argument 1 to "add_one" has incompatible type "str"; expected "int"

We have a nice, readable explanation of what we are doing wrong. Let's fix print_seven.

def print_seven() -> None:
    five = 5
    seven = add_one(add_one(five))
    print(seven)

If we run mypy on this, there will not be any complaints; we fixed the bug. This also results, happily, in working code.

The Python type system can get pretty deep, of course. It is not uncommon to encounter signatures like:

from typing import Dict, List, Mapping, Sequence

def unify_results(
    results1: Mapping[str, Sequence[int]],
    results2: Mapping[str, Sequence[int]]
) -> Dict[str, List[int]]:
    pass

In those cases, remember that everything is an object: yes, even types.

ResultsType = Mapping[str, Sequence[int]]
ConcreteResultsType = Dict[str, List[int]]

def unify_results(results1: ResultsType, results2: ResultsType) -> ConcreteResultsType:
    pass

We defined the input types as abstract types (using Mapping and Sequence). This allows sending in, say, a defaultdict, which maps strings to tuples. This is usually the right choice. We also chose to guarantee concrete return types in the signature. This is more controversial: sometimes it is useful to guarantee less in order to allow future changes to change the return type.

MyPy allows progressive annotation: not everything has to be annotated at once. Functions without any annotations will not be type-checked.

Go forth and annotate!

Review the previous articles in this series:

Topics

About the author

Moshe sitting down, head slightly to the side. His t-shirt has Guardians of the Galaxy silhoutes against a background of sound visualization bars.
Moshe Zadka - Moshe has been involved in the Linux community since 1998, helping in Linux "installation parties". He has been programming Python since 1999, and has contributed to the core Python interpreter. Moshe has been a DevOps/SRE since before those terms existed, caring deeply about software reliability, build reproducibility and other such things. He has worked in companies as small as three people and as big as tens of thousands -- usually some place around where software meets system administration...