Programming, Photography, Guides, Thoughts

Python type hints and typing support in examples


The first article in series about typing sup­port in Python will show you how to uti­li­se type hints in this otherwi­se dyna­mic lan­gu­age. Both Python 2 and 3 will be cove­red. You will also see why you may need a type sup­port in the first place.

Python is based on duck-typing. But if your inter­nal method works just with strings, the code will be more self-defensi­ve, if it will accept only strings and not any­thing that can be poten­ti­ally (and incorrect­ly) con­ver­ted to a string. Swap­ped argu­ments is a good example.

Type hints

Type hints are use­ful for API/public methods, because you are let­ting the world know what type you expect (if you do expect a spe­ci­fic type). Addi­ti­o­nally, the­re are tools that can check pro­per use of types and dis­play war­nings befo­re a cus­to­mer dis­co­vers the­ir impro­per use in form of bugs in pro­ducti­on. The­re are dif­fe­rent appro­a­ches, so let’s start with the most com­pa­ti­ble and least rest­ricti­ve one.

Documentation strings

This appro­ach is the most com­pa­ti­ble one, because it does not rely on any lan­gu­age syn­tax and works in all ver­si­ons of Python. Unfor­tu­na­te­ly, that also means it can­not be used by type chec­king pro­grams, such as MyPy (descri­bed later).

Docu­men­tati­on strings start and end with either tri­ple quo­tes """Docstring""" or tri­ple apo­stro­phes '''Docstring'''. The­re are dif­fe­rent for­mats of docstrings: Epy­text, reStructu­red­Text, Num­Py, or Goo­gle. If you­’re using PyCharm, you can set it in Tools/Python Inte­gra­ted Tools under Docstring for­mat. Sin­ce reStructu­red­Text is the default and also the most com­mon one, let’s use it in the following exam­ple. The docstring appro­ach is also called a Lega­cy Type Syntax.

A com­ple­te gui­de on how to use the Lega­cy Type Syn­tax is is avai­la­ble here.
I also cre­a­ted for you a han­dy, prin­table che­at she­et on reStructu­red­Text.

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 exam­ple abo­ve, cakes come in dif­fe­rent sizes and fla­vou­rs. Humans with dif­fe­rent names and they can like either a Cake or a Human. Addi­ti­o­nally, we can bake() Cakes. At the end, we make a use of these classes.

Can you spot all errors within seconds? I defi­ni­te­ly can­not. Luc­ki­ly, PyCharm’s code inspecti­on comes to rescue. If you spe­ci­fy a type in a docstring, it will make use of it. This is how the result looks like:

Python typing example using docstrings

The mis­ta­kes are now obvi­ous. Cake con­struc­tor takes a size argu­ment of type int, not str. We acci­den­tally swap­ped argu­ments in wrong_cake and me. Then we tried to bake() a sin­gle apricot_cake, when we can bake only list of cakes. And we obvi­ous­ly can­not bake dad. That’s insa­ne. What PyCharm did not find is try­ing to use „my cake” as one of the cakes, when we accept only Cake instances.

Neverthe­less, the bene­fits are clear now – you can dis­co­ver bug imme­di­a­te­ly and you pro­vi­de a clear inter­fa­ce to others. No one has to guess what a human can like or what you can bake. To take any guesswork out of the equati­on, you can also dis­play the docu­men­tati­on in PyCharm by pres­sing Ctrl+Q or from menu View – Quick Docu­men­tati­on:

PyCharm Quick Documentation

Typing module

Modu­le typing is based on PEP 484 and adds types sup­port into Python. It is avai­la­ble via pypi and from Python 3.5 it is avai­la­ble as stan­dard lib­ra­ry. Python docu­men­tati­on descri­bes use of the typing modu­le qui­te well, so let’s have a look at con­cre­te examples.

Types definition in comments

If you­’d like to use the typing modu­le pri­or 3.5, you have defi­ne types in spe­cial com­ments star­ting 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 argu­ments, it is possi­ble to defi­ne type for each argu­ment indi­vi­du­ally. This cre­a­tes rather long and ugly code. Ano­ther appro­ach it to defi­ne types for the who­le method at once. This defi­ni­ti­on is pla­ced between method sig­na­tu­re and docstring (or method’s body if the­re is no docstring). Note that self does not requi­re type definition.

The com­ment appro­ach has no pro­blem with for­ward or cir­cu­lar refe­ren­ces. An exam­ple of a for­ward refe­ren­ce is defi­ni­ti­on of type of the likes argu­ment in the __init__() method of class Human. One of the possi­ble valu­es is Human, which is refe­ren­ce to the class itself befo­re it is defined.

Type annotations

In Python 3.5 was intro­du­ced new syn­tax for type anno­tati­ons, based on PEP 526. Com­pa­ti­bi­li­ty with older Python ver­si­ons is achie­ved by the use of stub file with .pyi extensi­on (they are igno­re on older ver­si­ons). These files are sup­por­ted by PyCharm as well. Let’s have a look at con­cre­te 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 sig­na­tu­re, so it takes only few argu­ments or com­plex types and it no lon­ger fits on a sin­gle line. For this rea­son, it is a good idea to spe­ci­fy com­plex types befo­re­hand (such as the HumanLike type). Ano­ther advan­tage of defi­ning types in advan­ce is the­ir reu­sa­bi­li­ty. Note that this can be done pri­or Python 3.5 as well.

Type anno­tati­ons are lan­gu­age syn­tax and not just com­ments. The­re­fo­re, can run into pro­blems with for­ward or cir­cu­lar refe­ren­ces as men­ti­o­ned in pre­vi­ous secti­on. The­re are seve­ral ways of sol­ving this. The easiest and shor­test is spe­ci­fy­ing the type as a string: Union[Cake, 'Human'] . The down­si­de of this appro­ach is that PyCharm will not use this type in type checking.

Conclusion

Adding type hints to your pro­ject can impro­ve the code qua­li­ty and allow you to catch bugs more quick­ly. This is most pro­mi­nent when you use some IDE that high­li­ghts impro­per types usage, such as PyCharm. If you don’t need to sup­port older Python ver­si­ons, you can use type anno­tati­ons, which don’t even add that much code and are easy to use.

Make sure you check out the other articles in this series.


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.