Static Python type hints – advantages and drawbacks

While Python is a dynamically typed language, it is possible to add type hints to your code. This makes code more readable and allows for static analyzers to check your code for type errors, e.g. during development or in a CI pipeline. This article discusses the advantages and drawbacks of static Python type hints in detail, presents the most common static type checkers, and points you into the right direction to get started.

Introduction to static Python type hints

Python is a dynamically typed language. This means that you don’t have to declare types for variables or function arguments and return values, and the type of variables may change over time. Dynamic typing has a few advantages. You have to press fewer keys to produce working code, and you are very flexible in the way data flows through your system. The downside of this approach is that programming errors that are due to operations working on incompatible types go unnoticed until the code is executed. For instance, x.append(item) may fail if x is not actually a list. The problem gets worse with a larger code base, as the chance for incompatible types increases.

To address this problem, Python lets developers annotate variables and functions with voluntary type hints. These type hints have no effect at run-time. Python does not evaluate any type hints, and there is no negative performance impact. However, they can be used by third-party tools, static type checkers, to analyze your source code “at rest”, before it is executed. It also makes code much more readable, and thus easier to change. I will get into the advantages of static type hints below.

Support for these static type hints was added in Python 3.0, and has greatly improved in Python 3.5 and newer. In fact, all Python versions up to (including) 3.5 have reached their end-of-life (see here). Python 3.6 is now the oldest version you should use, and it has eliminated many static typing issues that existed in previous Python versions. Thus, now is the best time to get started with statically typing your code.

A simple example

To get familiar with how type hints look and feel, consider the following function definition (without type hints):


def search(inputs, database_connection): ...

To understand what this function is doing, you would have to read its implementation. With static type hints, the function can be defined like this:

def search(inputs: Dict[str, str], database_connection: sqlite3.Connection) -> List[Result]: ...

The added type hints declare that inputs is a dict that maps from str to str, database_connection is actually a Connection object from sqlite3, and it returns a list of Result objects. While this function definition takes a bit longer to read (and write), you have a much better idea of what the function expects and what it does, without having to read its entire implementation or documentation.

Advantages and drawbacks

Picking up new techniques requires a careful evaluation of their pros and cons. Let’s start with the advantages of static type hints:

  • Better readability – as we’ve just seen above, type hints take away the guess work of which type a variable may have, or what a function returns
  • Faster development speed due to better auto-complete results of your IDE – with type hints your IDE will know the types exactly and consequently it won’t offer you “guessed” choices
  • Precise automatic refactoring and “find usage” results – you will get much less unclassified, guessed results, so you can spend much less time on double-checking the affected code parts suggested by your IDE
  • Improved stability of your code base, by running static type checkers – IDEs such as PyCharm or VS Code come with these checks and perform them while you type, highlighting incompatible types as they arise. There are also dedicated tools you can use in your CI pipeline

Now the drawbacks:

  • It takes time to write type hints – fortunately, you can gradually add type hints to your code base over time, e.g. module by module. There are even tools such as pyannotate or MonkeyType which analyze your un-annotated code at run-time, collect the types of the variables and return values, and then annotate your code with the collected types
  • Passing static type checks may give you an illusion of safety – even with type hints in place and an analyzer not finding any problems, your application may still run into type errors at run-time. For instance, you may have provided incorrect type hints for data you load with pickle, or JSON data you retrieve from an API. There are also a few edge cases where a static type checker fails to detect an incorrect use of types
  • The start-up time of your application increases by a few milliseconds, due to the imports to the typing module that contains the type annotation classes, such as Dict
  • Some of the third party modules you use may not have type annotations yet – to ensure type safety when calling its APIs, you will have to spend some time to find external type definitions made by someone else, or even write your own stubs

Static type checkers

To make full use of type hints, you need to use a static type checker. I recommend that you already use one during development. If you use PyCharm, the integrated parser considers type hints automatically. You can inspect the list of issues via Code -> Inspect Code, which will include typing issues (among other issues types, such as bad PEP-8 formatting). If you use Visual Studio Code with Microsoft’s Python extension, you may want to use pylance, which adds static type checking capabilities to VS Code.

Since there is no guarantee that you (or your colleagues) really run a type checker in their IDE, you can enforce type checking in your CI setup. The four largest, still maintained type checkers are mypy (by Dropbox, since 2012), pytype (by Google, since 2015), Pyre (by Facebook, since 2018), and pyright (by Microsoft, since 2019). You may wonder why there are so many implementations, created 3 years after mypy. It seems that their developers built their own type checker, because they were unhappy with some aspect of mypy (e.g. speed). Another reason is that their companies apply their own type checkers to their (often internal) huge Python code bases. Adapting and tuning your own type checker is simpler, of course, than extending “someone else’s” (mypy’s) code.

Finding out which one is the “best” type checker for your project depends on your needs. You won’t get around experimenting with a few of them. If your time is limited, I recommend you test the oldest (mypy, most mature) and the newest (pyright, most shiny, many features) in parallel on your project. You will likely find out that each checker will detect different typing problems, and have different run-time requirements. For instance, mypy is built in Python, while pyright requires Node.js. In any case, you should not base your choice on blog posts (e.g. the posts illustrating the motivation of Facebook, etc. to build their own type checker). The contained information is quite dated, and some of the premises may no longer hold today.

Type hints for third party libraries

Your own Python code base will most likely depend on other, third party libraries, e.g. numpy, requests or pillow. To get the most out of static type checking, these libraries also need type hints. Unfortunately, many popular projects, such as PyQt5 or requests, do not have type hints yet. The general rationale seems to be that adding (and maintaining) type hints is a lot of work, or that the maintainers generally don’t care about type hints (see e.g. here).

Missing type hints are a problem, because a type checker cannot properly test whether you are calling functions with the correct types. To circumvent this problem, there are a number of third party projects that add missing type hints. For instance, typeshed includes hints for a few parts of the Python standard library where hints are missing, as well as a number of third party dependencies (e.g. pytz or requests). Its type definitions are already used by default by most type checkers (such as mypy). There are several other type hint projects, e.g. PytQt5-stubs, or django-stubs, which add type annotations for those specific libraries. If a library of your choice is missing type annotations and is not covered by typeshed, you may find a project filling this gap by searching the Internet for something like “python <library name> type stubs”.

Dynamic run-time type checks

Apart from static type checkers there are also dynamic checkers which enforce that the types of variables and return values match the declared type hints at run-time. Typically, you just need to add a decorator to your functions to have their type-compliance checked. There are also “master switches” that enable type checking globally. Check out Typical or typeguard, but be aware of the impact on run-time performance these tools will have.

Where should I start?

The official Python docs of the typing module, while complete, are meant as a reference. They only help if you’re already familiar with the concepts in general, and need to look up a few details – they are not suited to get you started. Instead, I recommend mypy’s cheat sheet, which concisely presents the most important concepts. If you want to dive deeper, consider Geir Arne’s detailed article about type checking.

What does the future hold?

Python’s typing module is being improved continuously. For instance, Python 3.7 adds support for PEP-563, allowing you to use a class’s name (without quotes) as type annotation for variables and return values within that class. Python 3.8 added many smaller typing classes, such as Literal or TypedDict, and support for protocols. Python 3.9 allows to use built-in generic types as type hints, s.t. you can write hints such as list[str] instead of List[str], avoiding the from typing import List statement.

Fortunately, you can use most of these newer features even on older Python versions (such as 3.6) by installing typing-extensions via pip. This avoids that your code breaks because of lacking backward compatibility, e.g. by using classes from typing which only exist since Python 3.8, trying to run the code on Python 3.6, which would cause ImportErrors.

Conclusion

Type hints are a great way to make your code base more robust. They also simplify the development process, thanks to improved auto-complete and more stable refactoring. You should definitely use it for long-living code, adding typing gradually, module by module. To unleash the full potential of type checking, not only your code, but also third party library APIs need type hints. Not every library developer seems to have gotten the memo (or cares about types), but over time type hints will be a part of every major library, making third-party type hints or stub repositories obsolete.

Leave a Comment