Skip to content

Type Hints

Abstract

This article explains what python type-hints are, how they can be enforced with the use of type checkers and the type checker of our choice: basedpyright and it's editor integration.

Most people only know python as a dynamically typed language, that doesn't offer any kind of type safety. In the very days of python, this was true, however today, things are a bit different. Even though Python on it's own is still a dynamically typed language, it does actually support specifying "type hints" which can even be enforced by external tools called "type checkers". With those, we can achieve a (mostly) type safe experience while using Python.

Regular python

In regular python, as most people know it, you might end up writing a function like this:

def add(x, y):
  return x + y

In this code, you have no idea what the type of x and y arguments should be. So, even though you may have intended for this function to only work with numbers (ints), it's actually entirely possible to use it with something else. For example, running add("hello", "world) will return "helloworld" because the + operator works on strings too.

The point is, there's nothing telling you what the type of these parameters should be, and that could lead to misunderstandings. Even though in some cases, you can figure out what the type should these variables have purely based on their name alongside the name of the function, in most cases, it's not that easy. It often requires looking through the docs, or going over the actual source code of such function.

Annoyingly, python won't even prevent you from passing in types that are definitely incorrect, like: add(1, "hi"). Running this would cause a TypeError, but unless you have unit-tests that actually run that code, you won't find out about this bug until it actually causes an issue and at that point, it might already be too late, since your code has crashed a production app.

Clearly then, this isn't ideal.

Type-Hints

While python doesn't require it, there is in fact a way to add a "hint" that indicates what type should a given variable have. So, when we take the function from above, adding type-hints to it would result in something like this:

def add(x: int, y: int) -> int:
  return x + y

We've now made the types very explicit to the programmer, which means they'll no longer need to spend a bunch of time looking through the implementation of that function, or going through the documentation just to know how to use this function. Instead, the type hints will tell just you.

This is incredibly useful, because most editors will be able to pick up these type hints, and show them to you while calling the function, so you know what to pass right away, without even having to look at the function definition where the type-hints are defined.

Not only that, specifying a type-hint will greatly improve the development experience in your editor / IDE, because you'll get much better auto-completion. The thing is, if you have a parameter like x, but your editor doesn't know what type it should have, it can't really help you if you start typing x.remove, looking for the removeprefix function. However, if you tell your editor that x is a string (x: str), it will now be able to go through all of the methods that strings have, and show you those that start with remove (being removeprefix and removesuffix).

This makes type-hints great at saving you time while developing, even though you have to do some additional work when specifying them.

Runtime behavior

Even though type-hints are a part of the Python language, the interpreter doesn't actually care about them. That means that the interpreter doesn't do any optimizations or checking when you're running your code, even if you have a function like add that we have added type-hints to, code like add(1, "hi") will not cause any immediate errors.

Most editors are configured very loosely when it comes to type-hints. That means they will show you these hints when you're working with the function, but they won't produce warnings when you pass in the wrong thing. That's why they're called "type hints", they're only hints that can help you out, but they aren't actually enforced.

Enforcing type hints - Type Checkers

Even though python on it's own indeed doesn't enforce the type-hints you specify, there are tools that can run "static" checks against your code. A static check is a check that works with your code in it's textual form. It will read the contents of your python files without actually running that file and analyze it purely based on that text content.

Using these tools will allow you to analyze your code for typing mistakes before you ever even run your program. That means having a function call like add(1, "hi") anywhere in your code would be detected and reported as an issue.

There is a bunch of these tools available for python, but the most common ones are pyright and mypy.

BasedPyright

The type checker that we use in our code-base is basedpyright. It's a fork of pyright which adds some extra checks and features and focuses more on the open-source community, than the official Microsoft owned Pyright.

Running BasedPyright

To run BasedPyright on the code-base, you can use the following command:

basedpyright .

You will need to run this from an activated poetry environment while in the project's root directory.

Editor Integration

On vscode, you can simply install the BasedPyright extension from the marketplace.

Note that this extension does collide with the commonly used Pylance extension, which is installed automatically alongside the Python extension and provide intellisense for Python. The reason BasedPyright collides with this extension is that Pylance actually uses pyright as a language server in the background, and as we mentioned, basedpyright is an alternative, so using both would cause duplicate errors. This means that you will need to disable Pylance, at least within our codebase.

If you're using Neovim, I would recommend setting up LSP (Language Server Protocol) and installing basedpyright, as it has language server support built into it. You can achieve this with the lspconfig plugin. You can then use mason-lspconfig to install basedpyright, or manually configure lspconfig and use your system-wide basedpyright executable.

Great resources

While type hinting might seem very simple from the examples shown above, there is actually a fair bit to it, and if you never worked within a type checked code-base, you should definitely check out some of these resources, which go over the basics.