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.