Python + Pyright now do true match exhaustiveness checking

Does everyone already know about this? Am I just late to the party? I randomly discovered this, and it’s blowing me away.

Background: why match & exhaustiveness checking is cool

First, here’s a great run-down on Python 3.10’s match statement. But Rust’s “matches are exhaustive“. I’ve always thought this is amazing, almost magical:

Rust knows that we didn’t cover every possible case and even knows which pattern we forgot!

That page and this one have good explanations. Haskell is the other language I’ve used where I’ve experienced “Non-exhaustive pattern” checking. That article also compares Haskell’s solution to Rust’s.

And now, as of a few months ago, in Python

def get_float(num: str | float):
match (num):
case str(num):
return float(num)
error: Cases within match statement do not exhaustively handle all values
Unhandled type: "float"
If exhaustive handling is not intended, add "case _: pass"

How it looks in VS Code:

Maybe I’m easily impressed, but IMO this is crazy cool. tldr; I’m getting this by combining Python 3.10’s match statement with Pyright’s MatchNotExhaustive check. Here’s a repo with my configuration and demo code.

Previous status quo: mypy and simulated exhaustiveness checking

I searched and everything I found is similar to this from Daily Dose of Python:

from enum import Enum
from typing import NoReturn
class Color(Enum):
RED = "RED"
GREEN = "GREEN"
BLUE = "BLUE" # I just added this
def handle_color(color: Color) -> None:
if color is Color.RED:
...
elif color is Color.GREEN:
...
else:
assert_never(color)
def assert_never(value: NoReturn) -> NoReturn:
assert False, f"Unknown value: {value}"

Here’s my refactor:

from enum import Enum
class Color(Enum):
RED = "RED"
GREEN = "GREEN"
BLUE = "BLUE" # I just added this
def handle_color(color: Color):
match (color):
case Color.RED:
...
case Color.GREEN:
...

That’s a pretty amazing difference, IMO. Now check out the error messages that each technique produces:

#
# Original
#
error: Argument 1 to "assert_never" has incompatible type "Literal[Color.BLUE]";
expected "NoReturn"
#
# Refactored
#
error: Cases within match statement do not exhaustively handle all values
  Unhandled type: "Literal[Color.BLUE]"
  If exhaustive handling is not intended, add "case _: pass"

Personally, I much prefer the refactored message: It clearly describes the actual issue. It gives two different ways to fix it.

Wrapping up

I tested several kinds of matches, pushing the exhaustiveness checking. See my demo repo for the code.

Leave a Comment

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s