PYTHON

Python Type Annotations

Date Published:
Last Modified:

Overview

Type hints/annotations for Python are introduced in PEP 484 and PEP 526. The python interpreter completely ignores type hints/annotations, it is type syntax which is formalized so that third-party tools such as IDEs or type checkers such as MyPy can then use it to provide type checking and other useful type inference capabilities.

The JetBrains range of IDEs support these type hints (e.g. PyCharm, IntelliJ IDEA).

Basic Types

Type annotations are specified for variables by adding a : and then a type after the variable name. The type for variables returned from a function are specified by adding a -> and then the type after the input parameter list of the function, but before the : which delimits the end of the function definition.

# The basic types (no import needed!)
my_int: int = 3
my_float: float = 1.234
my_string: str = 'Hello'

# Declaring the return type for a function
def my_func() -> str:
    return "foo!!"

There is also None, which is commonly used to define the return type of a function that doesn’t return anything.

def my_func() -> None:
    print('Hello, world')

Lists, Dicts, e.t.c

Although for basic types you do not have to import anything, for the more advanced types it is recommended you use the types defined in the typing module, which is included in the basic Python installation.

from typing import Dict, List, Tuple

# For a list, you speicfy the type of the elements stored
# in the list
my_list: List[int] = [ 1, 2, 3 ]

# For a dictionary, you specify the type of
# the key and the type of the value
my_dict: Dict[str, int] = {
    'foo': 2,
    'bar': 5,
}

# You specify the type for each variable in the Tuple
my_tuple: Tuple[str, int, int] = ('foo', 1, 2)

Optional

When a variable could be either of a particular type or None, use the Optional[<Type>] type.

# If my_var could return either be a string or None, use:
my_var: Optional[str]

Optional[<Type>] is the same as Union[Type, None] but is easier to type, and expresses the design intent of the code in a clearer fashion.

# Both my_var1 and my_var2 have the same type
my_var1: Optional[str]     
my_var2: Union[str, None]

If you want to ignore any type checking (this works with mypy):

x = causes_weird_type_error() # type: ignore

Generators

Python functions that use the yield keyword return a special type of object called a generator. You can use the Generator[] type annotation for the return type of generator functions.

The generator type has the syntax: Generator[yield_type, send_type, return_type]. If you don’t send anything to the generator, no return anything with the return statement, this simplifies to Generator[yield_type, None, None]:

from typing import Generator

def my_generator() -> Generator[int, None, None]:
    for i in range(10):
        yield i

You can also specify the return type as Iterable[yield_type]:

from typing import Iterable

def my_generator() -> Iterable[int]:
    for i in range(10):
        yield i

mypy

mypy is a third-party static type checker for Python, which utilizes the official Python type annotation specification described above (which applied to Python 3.6 or greater, but you can use mypy on lower versions of Python using a different, non-official type syntax).

mypy allows you to slowly introduce types into an existing code base without having to convert everything at once.

You can install mypy with:

$ pip install mypy

You can check a Python program with the command:

$ mypy my_file.py

Ignoring Code

You can tell mypy to ignore specific lines of code with # type: ignore:

my_weird_thing # type: ignore

The mypy.ini File

mypy can read a project level mypy.ini file which you can use to configure mypy. You can use this configuration file to enable/disable certain type checking features, to prevent certain files/directories from being type checked, and more.

The mypy Daemon (dmypy)

The mypy daemon (controlled with the executable dmypy), is a background server process which caches program state, making mypy run much faster on successive runs (e.g. rather than mypy taking an agonizing 30s to run, it runs in <1s). The mypy daemon is installed along with mypy.

You can run the mypy daemon with (assuming your working directory is the root directory you want to check in):

$ dmypy run -- --follow-imports=skip . | less

It is helpful to pipe the output to less so that you can clear the output once you have addressed the issues.

$ dmypy run -- --follow-imports=skip . | less

If you have a free terminal window, you can even incorporate watch so the errors update as you implement fixes:

$ watch dmypy run -- --follow-imports=skip .

mypy And Reusing Variables With Different Types

mypy does not like it when you re-use a variable for a different type:

key = 'hello' # Storing a string in key
my_dict2[key]

key = 4 # Now storing an int in key, mypy won't like this!
my_dict1[key]

One solution is to declare the variable as type Any the first time you use it:

from typing import Any
key: Any = 'hello'
key = 4 # mypy won't complain about this anymore

However, this could be considered a bad coding practice as you are loosing all the advantages of type checking when casting to Any. Although it adds more code, a better alternative is sometimes to create separate variables:

key_1 = 'hello'
key_2 = 3

Hopefully you can come up with more descriptive names than just key_1 and key_2.


Related Content:

Tags:

comments powered by Disqus