Type annotations in Python 3.6 and using Mypy as a static type checker

The main goal of type annotations is to open up Python code for static analysis. It makes it easier to debug and maintain code because each type is explicitly stated. It also makes the code review process simpler as the parameters and return types can be inferred from the function header. These changes were introduced in PEP 484.

In this regards, static type checking is the most important. It allows support for off-line third-party type checkers, such as Mypy, which will be introduced in a later section.

Purpose of annotations

The typing module in Python 3.6 contains many definitions that are useful in statically typed code. For instance, the Any type is used by default for every argument and return type of a function. This is all in regards to a checked function. If a function is to be ignored by a static type checker, the decorator @no_type_check should be provided before the function header, and the function will not be treated as having type annotations.

An example following the type annotations is shown below:

def scale(scalar: int, number: int) -> int:
    return scalar * number

This states that the expected type of the scalar and number argument is int and int, respectively. Analogically, the expected return type is also int.

The acceptable type hints that may be used are: None, Any, Union, Tuple, Callable, all of which are exported from the typing module (e.g. Sequence and Dict), type variables, and type aliases.

Using type aliases

Type aliases can be defined by using simple variable assignments:

Temperature = float

def forecast(temperature: Temperature, day: int) -> str: ...

Creating your own types

In Python 3.5 a new function is introduced called NewType. This allows the programmer to create distinct types:

from typing import NewType

UserId = NewType('UserId', int)
some_id = UserId(356) 

Static type checking using Mypy

When a script is run with a standard Python interpreter, the type annotations are treated primarily as comments. Using mypy, common code bugs can be found and it checks the code for proper return types etc. As mypy is a static analyzer, it does not cause any overhead when running the program.

Note: A function without a type annotation is considered dynamically typed.

Let’s run mypy for checking a class method I wrote:

def parse_file_properties(cls, properties: str) -> OrderedDict:
    properties = properties.split('\n')
    list_properties = []
    for property in properties:
        key_value = property.split('=')
            list_properties.append((key_value[0].strip(), key_value[1].strip()))
        except IndexError: # The case where a property contains a comment or a blank line
    return OrderedDict(list_properties) 

This function takes in a = separated string, parses it using split(), and returns it as an ordered dictionary. When I run mypy from a terminal, issuing the command python -m mypy hips/tiles, I get no error or warning, which indicates that the provided type annotations were correct.

However, if I change the function header to:

def parse_file_properties(cls, properties: int) -> OrderedDict:

I get this error message:

hips/tiles/description.py:61: error: “int” has no attribute “split”

The illustration below shows the basic workflow of static type checking:

Static type checking