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
database_connection is actually a
Connection object from
sqlite3, and it returns a
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:
Now the drawbacks:
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.
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.
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?
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
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
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.