The first article in series about typing support in Python will show you how to utilise type hints in this otherwise dynamic language. Both Python 2 and 3 will be covered. You will also see why you may need a type support in the first place.
Python is based on duck-typing. But if your internal method works just with strings, the code will be more self-defensive, if it will accept only strings and not anything that can be potentially (and incorrectly) converted to a string. Swapped arguments is a good example.
Type hints
Type hints are useful for API/public methods, because you are letting the world know what type you expect (if you do expect a specific type). Additionally, there are tools that can check proper use of types and display warnings before a customer discovers their improper use in form of bugs in production. There are different approaches, so let’s start with the most compatible and least restrictive one.
Documentation strings
This approach is the most compatible one, because it does not rely on any language syntax and works in all versions of Python. Unfortunately, that also means it cannot be used by type checking programs, such as MyPy (described later).
Documentation strings start and end with either triple quotes """Docstring"""
or triple apostrophes '''Docstring'''
. There are different formats of docstrings: Epytext, reStructuredText, NumPy, or Google. If you’re using PyCharm, you can set it in Tools/Python Integrated Tools under Docstring format. Since reStructuredText is the default and also the most common one, let’s use it in the following example. The docstring approach is also called a Legacy Type Syntax.
A complete guide on how to use the Legacy Type Syntax is is available here.
I also created for you a handy, printable cheat sheet on reStructuredText.
import time class Cake: def __init__(self, size, flavor): """ :param int size: Size of the cake. :param str flavor: Flavor of the cake. """ self._size = size self._flavor = flavor @property def size(self): """ :rtype: int """ return self._size class Human: def __init__(self, name, likes): """ :type name: string :type likes: Cake|Human """ self._name = name self._likes = likes def bake(cakes): """ :type cakes: list[Cake] """ for cake in cakes: time.sleep(cake.size) apricot_cake = Cake(10, "apricot") blueberry_cake = Cake("10", "blueberry") wrong_cake = Cake("apricot", 10) mum = Human("Mum", apricot_cake) dad = Human("Dad", mum) me = Human(blueberry_cake, "Radek") bake(apricot_cake) bake(dad) bake(["my cake", apricot_cake]) bake([apricot_cake])
In the example above, cakes come in different sizes and flavours. Humans with different names and they can like either a Cake
or a Human
. Additionally, we can bake() Cakes
. At the end, we make a use of these classes.
Can you spot all errors within seconds? I definitely cannot. Luckily, PyCharm’s code inspection comes to rescue. If you specify a type in a docstring, it will make use of it. This is how the result looks like:
The mistakes are now obvious. Cake
constructor takes a size
argument of type int
, not str
. We accidentally swapped arguments in wrong_cake
and me
. Then we tried to bake()
a single apricot_cake
, when we can bake only list
of cakes. And we obviously cannot bake dad
. That’s insane. What PyCharm did not find is trying to use „my cake” as one of the cakes, when we accept only Cake
instances.
Nevertheless, the benefits are clear now – you can discover bug immediately and you provide a clear interface to others. No one has to guess what a human can like or what you can bake. To take any guesswork out of the equation, you can also display the documentation in PyCharm by pressing Ctrl+Q or from menu View – Quick Documentation:
Typing module
Module typing is based on PEP 484 and adds types support into Python. It is available via pypi and from Python 3.5 it is available as standard library. Python documentation describes use of the typing module quite well, so let’s have a look at concrete examples.
Types definition in comments
If you’d like to use the typing module prior 3.5, you have define types in special comments starting with # type:
. See the following example:
import time from typing import Union, List class Cake: def __init__(self, size, flavor): # type: (int, str) -> None """ :param size: Size of the cake. :param flavor: Flavor of the cake. """ self._size = size self._flavor = flavor @property def size(self): # type: () -> int return self._size class Human: def __init__(self, name, # type: str likes # type: Union[Cake, Human] ): self._name = name self._likes = likes def bake(cakes): # type: (List[Cake]) -> None for cake in cakes: time.sleep(cake.size)
For method arguments, it is possible to define type for each argument individually. This creates rather long and ugly code. Another approach it to define types for the whole method at once. This definition is placed between method signature and docstring (or method’s body if there is no docstring). Note that self
does not require type definition.
The comment approach has no problem with forward or circular references. An example of a forward reference is definition of type of the likes
argument in the __init__()
method of class Human
. One of the possible values is Human
, which is reference to the class itself before it is defined.
Type annotations
In Python 3.5 was introduced new syntax for type annotations, based on PEP 526. Compatibility with older Python versions is achieved by the use of stub file with .pyi extension (they are ignore on older versions). These files are supported by PyCharm as well. Let’s have a look at concrete example:
import time from typing import Union, List class Cake: def __init__(self, size: int, flavor: str) -> None: """ :param size: Size of the cake. :param flavor: Flavor of the cake. """ self._size = size self._flavor = flavor @property def size(self) -> int: return self._size HumanLike = Union[Cake, 'Human'] class Human: def __init__(self, name: str, likes: HumanLike) -> None: self._name = name self._likes = likes def bake(cakes: List[Cake]) -> None: for cake in cakes: time.sleep(cake.size)
Types are now part of methods signature, so it takes only few arguments or complex types and it no longer fits on a single line. For this reason, it is a good idea to specify complex types beforehand (such as the HumanLike
type). Another advantage of defining types in advance is their reusability. Note that this can be done prior Python 3.5 as well.
Type annotations are language syntax and not just comments. Therefore, can run into problems with forward or circular references as mentioned in previous section. There are several ways of solving this. The easiest and shortest is specifying the type as a string: Union[Cake, 'Human']
. The downside of this approach is that PyCharm will not use this type in type checking.
Conclusion
Adding type hints to your project can improve the code quality and allow you to catch bugs more quickly. This is most prominent when you use some IDE that highlights improper types usage, such as PyCharm. If you don’t need to support older Python versions, you can use type annotations, which don’t even add that much code and are easy to use.
Make sure you check out the other articles in this series.
Leave a Reply