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."
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!