Link Search Menu Expand Document

Type annotations

Python has dynamic typing, which means that variables do not have to be defined with a specific type before using them. The type is determined at runtime based on its value.

The arguments of a function do not have a specific type; a and b can be of any type. Other languages such as Go or C require us to determine the exact type.

def multiply(a, b):
    return a * b

This makes Python a very flexible but also dangerous language. The following calls work, but the second one returns a result that we might not have expected.

print(multiply(2, 5))
# 10

print(multiply("2", 5))
# 22222

But the following gives an error, since multiplying two str is not defined.

print(multiply("2", "2"))
# TypeError

To avoid this kind of error at runtime, Python offers type annotations (PEP 3107). They are a kind of metadata associated with the input and output arguments of functions.

In other words, it allows you to tell people using your code what kind of data they should use as input.

# script.py
def multiply(a: int, b: int) -> int:
    return a * b

But they are not enforced. That is, even if we say that a and b are expected to be int, the following code can be executed just as before. The error is obtained at runtime.

print(multiply("2", "2"))
# TypeError

Fortunately, there are tools like mypy that allow detecting this type of incorrect call before the code is executed. It is some kind of static type checker.

mypy script.py

And in our case, it will tell us.

# error: Argument 1 to "multiply" has incompatible type "str"; expected "int"
# error: Argument 2 to "multiply" has incompatible type "str"; expected "int"

You can also access these annotations using the following.

print(multiply.__annotations__)
# {'a': <class 'int'>, 'b': <class 'int'>}

These annotations can be used at runtime. You can create a function that makes sure that a and b are int and raises an error otherwise. You can define any logic you want.

def multiply(a: int, b: int) -> int:
    annotations = multiply.__annotations__
    if not isinstance(a, annotations['a']):
        raise TypeError(f"a must be int, not {type(a)}")

    if not isinstance(b, annotations['b']):
        raise TypeError(f"b must be int, not {type(b)}")

    return a * b

print(multiply("2", "2"))
# TypeError: a must be int, not <class 'str'>.

There is an annotation for each type, and you can mix them with default values.

def power(base: float, exponent: int = 2) -> float:
    return base ** exponent

It also allows you to specify more complex types. This function takes a dict that uses str as key and str as value and returns a list of int.

from typing import List, Dict

def process(cfg: Dict[str, str]) -> List[int]:
    # ...

A summary of the benefits of annotations:

  • πŸ“ Documentation: They make reading easier by clearly indicating what type of data the function expects.
  • βœ… Validation: Tools like mypy can use the annotations to check that the types are correct. This way, we avoid surprises at runtime with things that can be prevented earlier.
  • ✨ Autocomplete: Editors like PyCharm and VSCode can take advantage of annotations to improve autocompletion and code navigation.

Although you will see a lot of code without annotations, it is advisable to use them. We recommend that you do so from now on.