Introduction
The programming languages such as C, C++, Java, etc. are statically typed languages meaning the data type of the variable must be declared before you can actually use it in your program. On the other hand, Python is a dynamically-typed language which means that you don’t have to declare the data type of the variable beforehand.
# static typed
int var; # var is declared as integer
var = 10; # integer 10 is assigned to var
var = 'hello'; # this throws error as you can't change the type
# dynamic typed
var = 10 # var is reference to integer object
var = 'hello' # var can now reference to string object
Statically typed and dynamically typed language have their own pros and cons. One of the issues with dynamically typed language is that type errors are caught only in the run-time. Python provides a way to handle this with the help of annotations. But keep in mind that Python doesn’t enforce annotations but hints to the developer about the data type of the variables so that type errors are caught before the run time. Let’s understand what annotations are, different types, and how they are helpful along with examples.
What are Annotations
As we just went through in the introduction section, annotations are Python features that hint developers about the data types of the variables or function parameters and return type. They also increase the readability of your Python program.
There are mainly two types of annotations in Python: function annotations and variable (type) annotations. We will go through the function annotations first and then dive into variable annotations.
Function Annotations
The syntax for function annotation is shown below:
def func(a: , b: ) -> :
pass
The above syntax for function annotation accepts two parameters a and b. The return type of the function is denoted as -> <expression>
. The <expression> can be anything. It can contain descriptions or data types of the parameters, etc. However, most commonly, function annotations are used to denote the data type of the function parameters and function return type.
Let’s look at a couple of examples. The below three examples takes two parameters and return their sum. As you can see, <expression> can be anything. All three are valid function definitions but it shows different ways of using function annotations.
def func1(num1: "1st param", num2: "2nd param") -> "return type":
return num1 + num2
def func2(num1: int, num2: int) -> int:
return num1 + num2
def func3(num1: int, num2: int=10) -> int :
return num1 + num2
In the first example, we have used some descriptive text. In the 2nd example, the data type of the parameters, and in the third example the data type of parameters with the default value as annotations.
Note that if there are any parameters with default values, then annotations must always precede a parameter’s default value as you saw in the 3rd example above.
How to access annotations
Function annotations can be accessed using __annotations__ attribute on the function object. The __annotations__ gives results in the form of a dictionary where keys and values are mapped to parameters and annotations respectively.
In the below example, num1 and num2 are the function parameters with int as annotation indicating the expected data type of the parameters. Note that even though the return is not a parameter it is added by Python to indicate the return type of function.
def func(num1: int, num2: int=10) -> int :
return num1 + num2
print(func.__annotations__)
And this outputs:
{'num1': int, 'num2': int, 'return': int}
As you may have guessed, if a function doesn’t have any annotations then __annotations__ attribute returns an empty dictionary.
def func(num1, num2):
return num1 + num2
print(func.__annotations__)
Variable Annotations
Type (variable) annotations were first introduced in PEP 484 where type comments are used to annotate the variables. As the name itself says, comments used indicate the data type of the variables. This method was not so effective and has its own drawbacks as listed here. So, we’ll not go into details of type comments. Instead, we’ll look into variable annotations that are introduced in PEP 526 in Python 3.6.
The simplest example of variable annotation is shown below. Some of the rules for writing the variable annotations are —
- There should be no space before the colon.
- There should be one space after the colon.
my_var: int=10
Now, let’s look into few more examples. In the first example below, the name is annotated as string type. In the second example, the city is annotated as a string with an initial value ‘Mysore’. In the third example, age is annotated as the integer with a default value of 35.
# Example 1
name: str
print(__annotations__)
{'name': }
# Example 2
city: str = 'Mysore'
print(__annotations__)
{'city': }
# Example 3
age: int = 35
print(__annotations__)
{'age': 35}
Example using typing module
In the above three examples for type annotations, we have used Python’s primitive data types such as int, and string. For complex types such as lists, tuples, etc, you should explore typing module. We will understand the usage of typing module in a separate blog post. However, let’s see one example using typing module that makes use of both function annotation and type annotation.
In the below example, the square function expects an integer parameter num and returns the squares of all the numbers from 0 to num. The variable squares is declared as List[int] indicating it holds a list of integers. Similarly, the return type of the function is also List[int]. Next, square.__annotations__ gives annotations local to the function and __annotations__ gives global annotations. Refer to the official documentation on advanced usage of typing module.
from typing import List
squares: List[int] = []
def square(num: int) -> List[int]:
for i in range(num):
squares.append(i**2)
return squares
print(square(10))
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
Local annotations:
print(square.__annotations__)
{'num': int, 'return': typing.List[int]}
Global annotations:
print(__annotations__)
{'squares': typing.List[int]}
Uses of Annotations
So far you have understood what annotations are, how to add annotations to your code, how to access annotations, etc. But how does this actually help you as a developer? Here are some uses of annotations.
Type checking
Third-party tools such as mypy can be used to catch type errors before runtime. In the below simple example, func takes two integer parameters and returns an integer. When you save this code and run it through mypy you will get an error for num2 as it is expecting an integer and you are passing a string ‘hello’.
Type hinting by IDEs
If the annotation was used to indicate the data type of the parameters IDEs hint at the data type of the parameter, etc. This will help you catch type errors as you write the code in the IDE such as Pycharm. Refer to the below example.
Auto-completion
Annotations also help in auto-completion and make your life easy. In the below example, the text variable is annotated as string type. So, IDE understands that it is a string data type. So it will start showing methods you can apply to the string object. Isn’t it awesome?
Readability
Annotations also make your code more readable. For example, when someone is looking at the below code, it will be clear that the function expects two integer parameters and return an integer value. You don’t have to use docstrings or comments for explaining the data types of the parameters.
def func(num1: int, num2: int) -> int:
return num1 + num2
Conclusion
Hope you got a good understanding of function annotations and variable annotations in this article. I suggest you try to make use of annotations in your next Python project and see how it improves your productivity. If you have any comments please let us know in the comments section below.
References
[1]. https://www.python.org/dev/peps/pep-3107/ (Function Annotations)
[2]. https://www.python.org/dev/peps/pep-0526/ (Syntax for Variable Annotations)
[3]. https://www.python.org/dev/peps/pep-0484/ (Type Hints)