This is the second part of a series of articles dealing with the type annotation system in Python, type hints for short. The first part gave an introduction to type hints.
This article is aimed at newcomers to type hints and wants to help you get started.
In this second part, I will go over a handful of carefully chosen examples of how to use basic type hints to solve a particular problem and improve the overall code quality and readability. Each example will cover a certain topic and look at type hints from a slightly different perspective. Most examples will end with a tip or best practice to help you improve your type hint skills!
The next article of this series will continue with more advanced uses of type hints.
First set of examples
In the first part, you have learned about static and dynamic typing and how and why to annotate a function with simple type hints. Here, I want to show you a few more examples of type hints in Python and say a word or two about why they might be useful in each case.
A quick side note about the typing
module ([4]): You can always use the basic types int
, float
, str
, bool
, bytes
without any import statement (see [6] for a nice cheat sheet!). This is the most basic level of type hints. More complex data types like list
, dict
, tuple
, and set
require their own type hint imported from typing
. For example, typing.List
is the correct type hint for the list
type. However, since Python 3.9 some of these data types can be used directly as type hints, so list[int]
is allowed in Python 3.9 but has to be List[int]
in previous versions. You might see both, so do not get confused. You can be sure that something is a type hint when it is preceded by a colon (except for slices and dictionary definitions, but I think they are easily told apart).
My editor of choice is Visual Studio Code (VSC) and I use the Python extension with the optional Pylance language server, so you know the setup with which I recorded the screenshots. More about how to setup VSC in a later part of this series.
Better auto-completion
First, I want to advocate type hints in favor for 100% accurate auto-completion for your favorite editor. Look at this example:
My editor cannot provide any method suggestions for text
because it is totally unclear what the type of text
might be.
Though, adding a simple : str
type hint to the function’s parameter solves the problem:
Meaningful return types
The same argument holds for return types of functions, both built-in and user-defined. You will gain 100% auto-completion (and a better understanding) about what is actually returned by a function call. Look at this example:
def do_some_stuff(numbers):
d = {x:x**2 for x in numbers}
return d.items()
What is the return type of this function? Let Pylance tell us:
The function do_some_stuff
returns both the keys and values of a dictionary. Without Pylance and its capabilities, you would get some information about the type of the variable what_is_this
, but only a very basic information of type tuple
. You would not know how many elements the tuple holds nor their types. In comparison, with Pylance you get a much more detailed feedback about the proper return type of the items()
method, which is ItemsView[int, int]
. How very useful indeed! You know now that what_is_this
holds an ItemView
instance for two int
elements.
If we change the function slightly to return the square root instead, Pylance provides us with a correctly updated feedback that the values of the dictionary are no longer integers but now of type float
:
No more need to read long documentations or try out some code in an interactive shell to find out what exactly is returned by a function, just let the type hints speak for themselves. However, this requires from us coders the curtesy and discipline of providing the right type hints in the first place.
Meaningful nested types
Let’s stay with this example for a little longer. So far, I have not annotated the function do_some_stuff
with type hints, I have just relied on Pylance’s capability of inferring the correct types. If I were to add type hints to this function, how should I annotate the numbers
parameter? Because I iterate it in line 2, it should be some kind of Iterable
. If I were to annotate it with list
, this would mean that you can only call this function with a list and no other iterable like range
, which was used in the example.
The typing
module provides all kinds of types you can use for proper type hints in more complex cases. For the case at hand, you can choose between typing.Iterable
and typing.Sequence
, depending on whether you want to stress __iter__
or __getitem__
being implemented by the attribute that is passed for the numbers
parameter. Because the range
function returns an object that implements both __iter__
and __getitem__
(so you can iterate it with for x in range(10)
and access its element directly with range(10)[n]
, which I did not know until now!), both type hints would be appropriate. In contrast, the zip
function only provides an implementation for __iter__
, so typing.Iterable
would be appropriate but not typing.Sequence
.
Having decided to go with typing.Iterable
, are you finished yet? Yes and no, depending on how specific you want to get. With Iterable
you inform about (and require, if type checked) your expectation to pass an iterable object, but not what is expected of its elements. For example, strings are iterables, too, but would fail in this context. You have to explicitly require an iterable which elements are of type numbers. If you do not specify the type of the elements (or any variable, for that matter), their type hint becomes Any
by default, or Unknown
in the case of Pylance, which is the implicit version of Any
.
So let me suggest the type hint for the numbers
parameter:
from typing import Iterable
def do_some_stuff(numbers: Iterable[float]):
d = {x:x**0.5 for x in numbers}
return d.items()
do_some_stuff([1,2,3,4.4])
This code will not produce any problems when type checked against. But when I try to pass a list with items other than numbers, I will get a type error:
I imported the Iterable
type hint from the typing
module and specified its elements to be of type float
. The notation OuterType[InnerType]
means that the outer object is some kind of container with inner objects of a certain type. However, OuterType
and InnerType
do not have to be different. So, when you want to type annotate a list and its elements, use list[<type>]
, for example list[int]
for a list of integers or list[str]
for a list of strings or even list[list[int]]
for a nested list of integers (and remember, that, previous to Python 3.9, you have to import List
from typing
instead of using the native list
type). If you want to type annotate a dictionary, you have to provide type hints for both the key and the values, e.g. dict[str, list]
for a dictionary that uses strings as its keys and a list as its values. It’s okay to leave the type of the elements of the list undefined, but try to be as specific and strict as possible.
Resources
Resources are covering the complete article series about type hints. Not all resources were referenced in this part.
[2] PEP 483 — The Theory of Type Hints
[3] Python Type Checking (Guide) — Real Python
[4] Typing module documentation
[6] Typing cheat sheet – Pysheeet
[7] Null in Python: Understanding Python’s NoneType Object — Real Python