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.