Type Checking In Python: Catching Bugs Before They Bite

Type checking in Python enhances code clarity and reliability. We catch errors early and maintain high software quality by using type hints and one or more type checkers. This article covers the benefits of type hints, and how to install and run mypy, pyright, and pyre.

Type Checking In Python: Catching Bugs Before They Bite
Battling the beast of hidden bugs armed with type checking.

Overview

In Python, dynamic typing allows for great flexibility but can lead to subtle and hard-to-find bugs. This is because Python's type checks are limited and happen at runtime.

What is Type Checking?

Type checking is verifying the types of variables and expressions in your code. This happens at compile time in statically typed; in dynamically r programs, they are not type-checked before running. 

The runtime might make some type assertions, but our code needs to deal with inputs having unexpected types; otherwise, our code might fail in surprising ways.

Although we shouldn't expect Python to achieve the rigor of Static type-checking of languages like Scala or Haskell, the languages have slowly gained type-checking capabilities Since Python 3.5 (PEP 484).

Python is interpreted, and Type-checking would be too costly if implemented directly. Since the type-checking behavior is defined but not implemented in the interpreter, different Type Checker implementations exist, each with pros and cons.


Installing Type Checkers: mypypyright, and pyre

Adding types to our Python programs is called Type Hinting. Because adding type information is optional and the interpreter does not use it, we must use a type-checking tool to reap the benefits of adding types to our programs.

The Python team's default implementation is mypy. Another notable implementation is Microsoft’s pyright, which has excellent Visual Studio Code integration. Finally, Meta created its tool called pyre, which is optimized to manage their big code bases.

The Reference Type Checker mypy

mypy is the most widely used static type checker for Python, it`s developed by the Python team. 

To install mypy, use pip, Python's package manager:

Command Line

pip install mypy
        

Once installed, you can run mypy on your Python files or directories:

Command Line

mypy some_python_script.py
        

This command will analyze your code and report any type errors, helping us catch issues before they become runtime problems.

Type Checking while Typing With Microsoft pyright

pyright is a fast type-checker developed by Microsoft. It's trendy in the Visual Studio Code ecosystem and can be easily installed as a Visual Studio extension:

But it can also be used as a standalone tool:

Command Line

npm install -g pyright
pyright some_python_script.py          
        

Type Checking Big Code Bases With pyre

pyre is a fast, scalable type checker built by Facebook. It's designed to handle large codebases efficiently and integrates well with modern Python development practices.

As with mypy we install it using pip:

Command Line

pip install pyre-check
        

pyre requires a setup before we use it. It will create a .pyre_configuration file in your project, which you can customize according to our needs:

Command Line

pyre init
        

The primary use case for pyre is running it on our complete program instead of type-checking file by file:

Command Line

pyre 
        

A blacksmith carefully sharpening a large sword labeled 'Type Hints' in a workshop, symbolizing the precision and strength type hints bring to Python code.
Sharpening the sword of code clarity. Type Hints forge more robust and precise Python programs.

Adding Type Hints to Our Python Code

We must add Type Hints to our code to benefit from Type Checkers. As a bonus, they also improve our code's readability by clarifying some of our code constraints.

Simple Type Hints

Let's start by looking at a simple add function and how can it be called with the wrong types:

Python

def average(a, b):
    return (a + b) / 2
          
result_avg = average(12, 5)
print(result_avg + 2)

result_avg = average(10, "5")
# TypeError: unsupported operand type(s) for +: 'int' and 'str'        
        

Running a mypy or any other Type Checker won't bring us any benefit since the code above has no type information for it to make any judgments about our code.

Starting with Python 3.5, we can annotate variables, function arguments, and function return values. Functions and arguments are annotated by adding a colon after the name and writing a type after the colon:

Python ≥ 3.5

def average(a: float, b: float):
    return (a + b) / 2
          
result_avg = average(12, 5)
print(result_avg + 2)

result_avg = average(10, "5")
# TypeError: unsupported operand type(s) for +: 'int' and 'str'        

If we just run the code above with Python, we’ll get the same Type Error as before, but now we have the chance to run a Type Checker, which will tell us our code is incorrect, where it is erroneous, and why:

A terminal screenshot showing a mypy error message indicating an incompatible type: 'str' was provided where 'float' was expected.
Type Checking in Action: mypy catches an incompatible type error, ensuring code reliability before runtime.

This is an improvement, but we still leave some ambiguity in our code because we still need to add type hint for the returned value. Given Python already uses the colon to terminate function declarations, a new symbol (->) was added for writing type hints on return types:

Python ≥ 3.5

def average(a: float, b: float) -> float:
    return (a + b) / 2
          
result_avg = average(12, 5)
print(result_avg + 2)

On the right side of the colon, we can use built-in types/classes, abstract base classes, user-defined classes, and types from the typing module.

Collection Type Hints

That was an improvement, but what if our function needs to calculate the average over a collection of elements instead of only two? If we add list as a type hint, it will prevent us from passing non-list objects to the function, but we would still be able to give a non-numeric object as part of that list:

Python ≥ 3.5

def list_average(nums: list):
    return sum(nums) / len(nums)

in_nums = [2, "3", 5, 20]
avg = list_average(in_nums)
print(avg + 2)

We need a way to add type information about the elements of collection classes. In statically typed languages, this is known as Generics, and support for generic typing was included in 3.5 (PEP 484) and improved in 3.7 (PEP 560), 3.11 (PEP 646), and 3.12 (PEP 695).

We will discuss Generics in depth in a future article. For now, we’ll limit ourselve to the simplest case of adding a type hint to the element of a standard Python collection. For this, we need to import the collection abstract class from the typing module and add the type of the elements between square brackets after the collection type:

Python ≥ 3.5

from typing import List

def list_average(nums: List[float]) -> float:
    return sum(nums) / len(nums)

in_nums = [2, "3", 5, 20]
avg = list_average(in_nums)
print(avg + 2)

Now, we can catch the error while type checking:

A terminal screenshot showing a mypy error message indicating an incompatible type: a list of objects was provided where a list of floats was expected.
mypy detects an incompatible list element type, ensuring only a list of floats is passed to the function for accurate calculations

Conclusion

Type hinting in Python is a powerful tool for enhancing code quality, readability, and maintainability.

By leveraging type hints and tools like mypypyright, and/or pyrewe can catch typing errors early in the development process, preventing them from becoming runtime issues. Additionally, it facilitates collaboration by communicating a contract for our functions and data structures.

Incorporating type-checking into our development workflow doesn’t require a complete codebase overhaul. We can start small, gradually adding type hints to critical functions.

In our next article, we will see how to integrate Python’s type checking into a continuous integration workflow with GitHub Actions. We`ll use it to keep one or more branches type-safe, to the extent of the type hinting we add, with minimal or no manual intervention.

Ultimately, adopting type checking helps us improve as developers and help us learn concepts transferable to other programming languages like Haskell, or Scala.


Addendum: A Special Note for Our Readers

I decided to delay the introduction of subscriptions. You can read the full story here.

In the meantime, I'll be accepting donations. I would appreciate it if you consider a donation, provided you can afford it:

Every donation helps me offset the site's running costs and an unexpected tax bill. Any amount is greatly appreciated.

Also, if you are looking for a fancy way to receive 2025, I am renting my timeshare in Phuket to cover part of the unexpected costs.

Anantara Vacation Club Phuket Mai Khao $2000.00/night
Phuket, Thailand / Posting R1185060

Or share our articles and links page in your social media posts.

Finally, some articles have links to relevant goods and services; buying through them will not cost you more. And if you like programming swag, please visit the TuringTacoTales Store on Redbubble.

Take a look. Maybe you can find something you like: