Code Better with Type Hints – Part 1

By on 12 August 2021

This is the first part of a series of articles dealing with the type annotation system in Python, type hints for short.

With this opinionated article, I advocate the use of type hints. I want to explain why you should care and why your code will be better, more bug-free, more accessible, and easier to maintain. At the end, I will give you some recommendations on how to get started.

Type hints is a really huge topic and a quick look at the official documentation [4] is all it takes to feel a little lost. To be honest, I am not all too familiar with the more advanced topics myself, like Generic or Protocols! So fear not if you feel overwhelmed by the amount of information about this topic because, fortunately for us, getting started with type hints is really easy and does not require a lot of knowledge. This article is aimed at newcomers to type hints and wants to help you get started.

In this first part I will give a short introduction to type hints, comparing the two concepts of static typing versus dynamic typing, and give examples of how to annotate types in Python.

Short Introduction to Type Hints

Every programming language must have some notion of types so it knows how to work with objects, which operations are allowed, and which are forbidden.

So even without any explicit type definitions, Python for example “knows” that it’s possible to add different kinds of numbers:

>>> type(1)
<class 'int'>

>>> type(2.2)
<class 'float'>

>>> 1 + 2.2
3.2

But that it’s not possible to add a string and a number:

>>> 1 + "2.2"
TypeError: unsupported operand type(s) for +: 'int' and 'str'

Python informs you that the last expression is not valid and raises a TypeError at runtime. This is possible because Python has a type system in place and can inform you about unsupported operations. The problem now is that this happens only at runtime and not before you ship the code! That is the consequence of Python being a dynamically typed language.

Dynamic Typing

Python is a dynamically typed language. This means two things: that the Python interpreter does type checking only as code runs, and that the type of a variable is allowed to change during its lifetime.

The first point means that Python will not be able to detect a problematic error like the following one before it’s already too late:

if False:
  1 + "two"  # This line never runs, so no TypeError is raised at runtime, ever

In a statically typed language like Java the compiler will inform you about the problematic TypeError as soon as you write the code down. In Python, however, you will not be informed about this problem until it actually occurs. If the condition were to be changed in the future and would be evaluated to True, the program will raise a TypeError all of the sudden besides having run without any problems up to this point. So there could be a lot of hidden type errors in your code that you are not aware of because they were never triggered in the past.

The second point is also a source of concern. A variable can change its type any number of times:

>>> thing = "Hello"
>>> type(thing)
<class 'str'>

>>> thing = 28.1
>>> type(thing)
<class 'float'>

In Python, there is nothing that prevents a variable from changing its type. That is often exactly what we want and why we are in favor with Python for its simplicity and ease of use. However, again, this might lead to problematic behavior of your program when a variable, that once was of type x, suddenly changes its type to y, and is therefore a completely different object.

This might not be a big problem for code that is run as its author intended it to be run (like a command line application). But think about all the programs where the user interacts with the program by providing input or by using components of the code for their own program. There is no way to know about the zillion ways your code will be used by the users out there. And as there is no way in Python to protect object variables and methods against being accessed and altered, or to protect an object against being sub classed, you cannot foresee possibly dangerous type changes. With type hints, however, it becomes possible with the help of the right tools to ensure and check valid types both before and even at runtime, as I will show in the remainder of this article.

Type Hints in Python

Python will always remain a dynamically typed language. However, PEP 484 [1] introduced type hints, which pave the way for performing static type checking of Python code. Unlike how types work in most other statically typed languages, type hints by themselves don’t cause Python to enforce types. As the name says, type hints just suggest types.

So let’s see an example of how to use type hints in Python!

The following function turns a text string into a headline by adding proper capitalization and a decorative line:

def headline(text, title=True):
    if title:
      text = text.title()

    return f"{text}\n{'-' * len(text)}"

By default, the function returns the headline title cased. By setting the title flag to False, you can alternatively print the headline text unaltered:

>>> print(headline("My little headline"))
My Little Headline
------------------

>>> print(headline("My little headline", title=False))
My little headline
------------------

It’s time for our first type hints! To add information about types to the function, annotate its arguments and return value as follows:

def headline(text: str, title: bool = True) -> str:
  # same code as before

This new syntax tells the reader of this code that you expect the parameter text to be of type string and title to be of type bool. In addition, with () -> str you inform about the return type of the function, namely string again. With this alone, the reader has a much clearer picture of what is going on here and what your function does: The function headline accepts a text and a Boolean and finally returns a text again.

You might argue that this is rather obvious but I beg to differ.

First, it would be easy to make this example much more complicated and provide a function with much
more code where you would have a hard time to actually find the line where each function attribute is used just to understand how it is used and what its type might be. With type hints, you know just from reading the function’s definition how to call this function because you know all types of all parameters.

Second, there is a more subtle problem in general: naming variables. I deliberately choose the bad parameter name title instead of a more appropriate name like titlecased (which indicates a Boolean nature). However, you will not always be able to find a name that successfully communicates the nature of the variable to the reader, which is getting increasingly harder with more complex code. Whereas text assumably should be a string (but do you know for sure before you have tried it?), title could be many things depending on what you think the function does. Now you could go on reading the docstring of the function hoping for some clarification about what to expect from this parameter, but honestly, there might not be a docstring or at least not a very informative one. And having the type hint bool as an annotation right behind the title parameter makes things so much easier for everyone involved.

Third, even without dedicated tools to do a static type check, you can benefit from the additional type hints when you have an editor or an Integrated Development Environment (IDE) that runs the static type checks for you in the background. I will come back to this in a minute.

Before, I want you to clearly understand one point and thus repeating myself: adding type hints like this has no runtime effect. Type hints are only hints and are not enforced on their own.

Let’s demonstrate what that means: due to the ability of Python to evaluate any expression to either True or False, there is a real and hard to notice bug with my function because you can call the function with a string (or any other value, for that matter) instead of a Boolean and get this:

>>>  print(headline("My first headline", "My Title"))
My First Headline
-----------------

The headline is title cased although the attribute title was not set to True. This works because Python evaluates all strings to True, unless its the empty string "", which will be evaluated to False. And only because it works today is no guarantee that it will still work tomorrow! And it is clearly not what was intended by the function’s author. More importantly, you cannot always tell what the function’s author’s intentions were given the code alone because it could be one of several things. Type hints make intentions explicit. And you might remember the second Zen of Python:

Explicit is better than implicit.

Zen of Python

This concludes the first part of this series. You should have learned about type hints in general as well as how to start using type hints in Python. In the next part I will guide you through a set of examples of how to actually use type hints, why I think type hints are beneficial in each case, and how to become more and more familiar with type hints.


I hope you have enjoyed reading the article! If you have any questions, comments or suggestions, don’t hesitate to contact me via the PyBites Slack, LinkedIn or GitHub. You can also leave your comment right on this page.

Let me know if you want to read more about this topic or any other topic, for that matter.

Keep Calm and Code in Python!

Resources

Resources are covering the complete article series about type hints so do not wonder if not all resources where referenced in this part.

[1] PEP 484 — Type Hints

[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

Want a career as a Python Developer but not sure where to start?