Python Type Hints - How to use Mypy’s unreachable code detection

2021-05-19 My code before removing the unreachable parts (...and after)

I recently discovered Mypy has a secondary function as an unreachable code detector. This feature is a great way to highlight places bugs may be hiding, as code paths that can’t possibly run normally show a logical error.

We can activate this feature by setting the warn_unreachable option to true. Contra to the name, the option makes Mypy log an error for each unreachable statement or clause. We can set the option in a setup.cfg like so:

[mypy]
warn_unreachable = true

We can also pass --warn-unreachable on the command line.

Mypy can discover many kinds of unreachable code. For example, take this function with two return statements:

def double_return() -> int:
    return 1
    return 2

When we run Mypy on this file, it highlights line 3 as unreachable:

$ mypy --warn-unreachable example.py
example.py:3: error: Statement is unreachable
Found 1 error in 1 file (checked 1 source file)

Fixing requires us to investigate. Two return lines could have arisen from a bad merge of two branches. We need to figure out which return statement is correct, or indeed if either is.

Mypy’s reachability detection is fine-grained and can highlight just one clause on a line. For example take this code:

def bad_check(x: int, y: int) -> None:
    if isinstance(x, int) or x < y:
        ...

The first isinstance() clause in the if is always True, so the x < y clause is unreachable. Mypy highlights it as such:

$ mypy --warn-unreachable example.py
example.py:2: error: Right operand of 'or' is never evaluated
Found 1 error in 1 file (checked 1 source file)

Such unreachable clauses can arise through refactoring - perhaps the type of x has changed from int | None to int and the isinstance() check is no longer required.

Another case that Mypy can detect is when we check for a type that the variable’s hints say it may not be. For example:

def not_noneable(x: int) -> None:
    if x is None:
        print("x is None")

Mypy tells us this if clause is unreachable:

$ mypy --warn-unreachable example.py
example.py:3: error: Statement is unreachable
Found 1 error in 1 file (checked 1 source file)

This will require another investigation. Either the variable is missing the option to be None in its type hint, or this if clause can be removed.

Mypy’s unreachable code detection is not perfect. The checks are based on types, not values, so they cannot detect duplicated checks on values that are obvious to the human eye. For example:

def double_check(x: int) -> None:
    if x == 0:
        print("x is zero")
    elif x == 0:
        print("well x is zero")

The elif can never be true as the value 0 has already been handled, but Mypy does not highlight this.

The best defence against all unreachable code remains 100% code coverage. (Yes, seriously 100%!). But Mypy’s reachability detection can be a fast way of checking your code for potential bugs before engaging in more costly testing.

Fin

May all your code be found reachable,

—Adam


🎉 My book Speed Up Your Django Tests is now up to date for Django 3.2. 🎉
Buy now on Gumroad


Subscribe via RSS, Twitter, or email:

One summary email a week, no spam, I pinky promise.

Related posts:

Tags: mypy, python