Type checking in Python with mypy
When I switched from Java to dynamic languages, I sometimes missed type checking. I flirted with rocket-science type systems, like you’ll find in Scala or Haskell.
Now, Python 3.6 supports optional type checking with two new components. The typing library lets you annotate parameters and return types. Mypy does the actual type checking.
from Łukasz’s slides
After hearing Łukasz Langa present Gradual Typing of Production Applications, I got the itch to try these new tools out.
It’s easy to get started. First, install mypy:
$ pip install mypy
If we want to say, for example, that the greatest-common-divisor function takes two ints and returns an int, we can:
def gcd(a: int, b: int) -> int:
while b:
a, b = b, a % b
return a
print('gcd(24, 42) = ', gcd(24, 42))
Now, if we fall asleep on our keyboard…
wut = gcd('zzz', 42)
…we want the type checker to give us a heads-up. We invoke mypy on the command line and get what we deserve - a nice error message:
$ mypy type_check_me.py
type_check_me.py:8: error: Argument 1 to "gcd" has incompatible type "str"; expected "int"
You can also create typed collections, such as a list of strings:
from typing import List
def caps(strings: List[str]) -> List[str]:
return [s.upper() for s in strings]
Parameterized types are done using TypeVar
.
from typing import List, TypeVar
T = TypeVar('T')
def first(list: List[T]) -> T:
return list[0] if list else None
The above function works and type checks, but we’re lying a little bit. The function doesn’t always return a value of type T. It returns None
as the first element of an empty list. Mypy lets us get away with that by treating None
as a special case.
We can be explicit about possible None
values using Optional
, an experimental feature similar to Scala’s Option
or Haskell’s Maybe
:
from typing import List, TypeVar, Sequence, Optional
T = TypeVar('T')
def first(seq: Sequence[T]) -> Optional[T]:
return seq[0] if seq else None
I don’t know why, but I had to use Sequence
rather than List
. Mypy complained about variance.
Our consumers of our first
function are on notice that they need to handle None
:
curses = ['dangit', 'shucks', 'flapdoodle']
def twice(a: Optional[str]) -> str:
return a + ', ' + a if a else 'um, um'
double_curse = twice(first(caps(curses)))
There’s yet another choice besides strict-optional and not strict-optional. The first
function could throw an exception if no first element exists.
def first_or_die(seq: Sequence[T]) -> T:
return seq[0]
I imagine these awkward bits will get worked out as recommended practices get established. In Haskell, the only slightly exaggerated saying is, “If it type-checks, it runs”. Bringing some of that magic to Python is pretty cool.