Annotations
In this lesson, you’ll learn about annotations in Python. Annotations were introduced in Python 3.0 originally without any specific purpose. They were simply a way to associate arbitrary expressions to function arguments and return values.
Years later, PEP 484 defined how to add type hints to your Python code, based off work that Jukka Lehtosalo had done on his Ph.D. project, Mypy. The main way to add type hints is using annotations. As type checking is becoming more and more common, this also means that annotations should mainly be reserved for type hints.
Function Annotations
For functions, you can annotate arguments and the return value. This is done as follows:
def func(arg: arg_type, optarg: arg_type = default) -> return_type:
...
For arguments, the syntax is argument: annotation
, while the return type is annotated using -> annotation
. Note that the annotation must be a valid Python expression.
When running the code, you can also inspect the annotations. They are stored in a special .__annotations__
attribute on the function:
>>> import math
>>> def circumference(radius: float) -> float:
... return 2 * math.pi * radius
...
...
>>> circumference.__annotations__
{'radius': <class 'float'>, 'return': <class 'float'>}
>>> circumference(1.23)
7.728317927830891
Sometimes you might be confused by how Mypy is interpreting your type hints. For those cases, there are special Mypy expressions: reveal_type()
and reveal_locals()
. You can add these to your code before running Mypy, and Mypy will dutifully report which types it has inferred. For example, save the following code to reveal.py
:
# reveal.py
import math
reveal_type(math.pi)
radius = 1
circumference = 2 * math.pi * radius
reveal_locals()
Next, run this code through Mypy:
$ mypy reveal.py
reveal.py:4: error: Revealed type is 'builtins.float'
reveal.py:8: error: Revealed local types are:
reveal.py:8: error: circumference: builtins.float
reveal.py:8: error: radius: builtins.int
Remember that the expressions reveal_type()
and reveal_locals()
are for troubleshooting in Mypy. If you were to run the Python script interpreter, it would crash with a NameError
:
$ python3 reveal.py
Traceback (most recent call last):
File "reveal.py", line 4, in <module>
reveal_type(math.pi)
NameError: name 'reveal_type' is not defined
Variable Annotations
In the definition of circumference()
in the previous section, you only annotated the arguments and the return value. You did not add any annotations inside the function body. More often than not, this is enough.
However, sometimes the type checker needs help in figuring out the types of variables as well. Variable annotations were defined in PEP 526 and introduced in Python 3.6. The syntax is the same as for function argument annotations. Annotations of variables are stored in the module level __annotations__
dictionary:
>>> pi: float = 3.142
>>> def circumference(radius: float) -> float:
>>> return 2 * pi * radius
>>> circumference.__annotations__
{'radius': <class 'float'>, 'return': <class 'float'>}
>>> __annotations__
{'pi': <class 'float'>}
>>> circumference(1)
6.284
>>> nothing: str
>>> nothing
Traceback (most recent call last):
File "<input>", line 1, in <module>
nothing
NameError: name 'nothing' is not defined
>>> __annotations__
{'pi': <class 'float'>, 'nothing':<class 'str'>}
00:00 This video is about using annotations in your Python code. A little background on annotations. First introduced in Python 3.0, they were used as a way to associate arbitrary expressions to function arguments and return values. A few years passed and then PEP 484 defined how to add type hints to your Python code.
00:24 The main way to add the type hints is by using the annotations, like you saw before in the type hint videos. And although there may have been other uses for annotations in the past, as type checking is becoming more and more common, annotations should mainly be reserved for type hints.
00:42 Now this may seem as a bit of a review, annotation style is the same as you did for the type hints before. To do function annotations, you annotate the arguments and the return value.
00:56
The syntax for arguments is the argument’s name, colon (:
), space, and then whatever the annotation is. And then the syntax for return types is that space, greater than symbol arrow (->
), and then the annotation. As a note, annotations must be a valid Python expression.
01:17
One kind of interesting note is that you can do inspections of your annotations. To inspect the annotations while running your code, you can use the special .__annotations__
attribute on the function.
01:29
So again, that would be a dot (.
) with two underscores (__
) and then the word annotations
and then two underscores (__
).
01:36
Let me have you work with annotations a little bit more with an example. Working here in the REPL, I’ll have you import math
and then define a function named circumference()
.
01:49
circumference()
takes an argument of radius
, which is a float
, and this function returns a float
.
01:59
Then the return is 2 * math.pi * radius
. Great. So, here’s circumference()
. It’s expecting a radius
. When running the code, you can actually inspect these annotations that you’ve put in already.
02:16
They’re stored in a special dunder method of .__annotations__
, so you could type circumference.
and then two underscores and you’ll see .__annotations__
as the very first choice here. And here you can see, there’s the two annotations that you put in for the types.
02:35
The radius
being of a float
and the return value being of a float
. If you’re to use circumference()
and give it a value of 1.23
, you can see there a float
going in and a float
coming out.
02:52
So, how is Mypy interpreting your hints? If you have questions about that, there are a couple special Mypy expressions that can provide a little more clarity. One is reveal_type()
and the other is reveal_locals()
. Let me give you an example.
03:09 Let’s create a new file.
03:13
This file is going to be named reveal.py
.
03:18
Let me adjust everything here. Okay. So, for reveal.py
, again import math
. And here you’re going to use the Mypy expression of reveal_type()
of math.pi
.
03:33
I’ll have you set the radius = 1
.
03:45
And then you can use another statement that’s part of Mypy of reveal_locals()
. So, please note that this is a troubleshooting step. If you were to try to run this Python script, reveal.py
, you’re going to end up with errors because those are not valid Python functions. But let’s look at what happens in the terminal when you run mypy
. Make sure you’ve saved reveal.py
.
04:09
Oh, I need to exit my REPL. There you go. So here, mypy reveal.py
. Here at line 4, Revealed type is 'builtins.float'
, from the math
library.
04:24
And here on line 9, it’s asking to reveal the locals, and the local types are the circumference
, which is a float
, and the radius
, which is an int
in this case.
04:33
So Mypy has correctly inferred the types of all these built-in floats and ints. And again, if you were to try to run reveal.py
with these two statements in it, you are going to get some errors.
04:46
Here it’s going to say 'reveal_type' is not defined
. So, you need to make sure you remove those statements before saving your code. A recent development is the ability to add annotations for variables, also.
05:00
This is defined in PEP 526 and introduced in Python 3.6. It uses the same syntax as for function arguments, with the name—in this case, pi
—colon, then the type. If you’re going to assign a value, then you would have an equal sign (=
) with the spaces before and after, and the value.
05:22
You might remember that .__annotations__
dictionary that you checked out before for a function. Well, annotations of variables are stored in the module level in an __annotations__
dictionary.
05:33
Let me have you try this out with some code. So, say you had a variable named pi
. Here, you’d put a colon after it and say pi
is a float
, and you can set its value. Again, when you’re defining circumference()
here,
05:49
you’d do it the same way you did before, circumference(radius: float)
with a return of a float
.
05:55
This time, you’re going to use pi
instead of math.pi
, times the radius
. So here, the annotations for circumference()
would be stored in its own dunder method of .__annotations__
that you saw before.
06:08
But if you type __annotations__
, itself, as a function, it’ll be stored in the module-level __annotations__
dictionary. And here you can see pi
and its annotation of being of a <class 'float'>
.
06:29
You’re also allowed to annotate a variable, even without giving it a value. This will add the annotation to the __annotations__
dictionary also, while the variable still is going to remain undefined.
06:42
So make a variable named nothing
, with a colon, and say that it’s of type str
(string) as an annotation. So if you type nothing
by itself, you’re going to get a NameError
, 'nothing' is not defined
. But if you were to look it up in __annotations__
, you’ll see here along with 'pi'
of <class 'float'>
, 'nothing'
is of a <class 'str'>
.
07:06 In the next video, I’ll take you back to looking at type comments.
Become a Member to join the conversation.