The first article in series about typing support in Python will show you how to utilize 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 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.
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.
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 flavors. 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
str. We accidentally swapped arguments in
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
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:
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.
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.
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.
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.