-
- Notifications
You must be signed in to change notification settings - Fork 3k
Description
Feature
Add a new check, [disallow-non-exhaustive-match], that enforces at type check time that no match statements implicitly fall through their bottom.
I would prefer if this check were on-by-default, or at least enabled when strict = true, but if this is controversial to the mypy team, off-by-default would be acceptable.
Pitch
The (superseded) PEP 622 has a great discussion on the motivation for this feature with several examples.
For brevity, below we list a very simple example demonstrating the kinds of correctness checks that motivate this feature request.
Consider we have the following code:
from enum import Enum, auto class Color(Enum): Red = auto() Green = auto() def print_color(color: Color) -> None: match color: case Color.Red: print("red") case Color.Green: print("green")Now, consider in the future another Color variant is added:
from enum import Enum, auto class Color(Enum): Red = auto() Green = auto() Blue = auto() def print_color(color: Color) -> None: match color: case Color.Red: print("red") case Color.Green: print("green")Oops! We forgot to add another case to print_color's match, Color.Blue is not handled, and this bug slips though into runtime.
Even if mypy is configured in strict mode, this bug isn't caught (because Python's default behavior is to allow match fall through).
[tool.mypy] strict = true$ mypy main.py Success: no issues found in 1 source fileSo how does a programmer defend against this bug? Currently, the best solution is to sprinkle assert_never clauses into every match.
from enum import Enum, auto from typing_extensions import assert_never class Color(Enum): Red = auto() Green = auto() Blue = auto() def print_color(color: Color) -> None: match color: case Color.Red: print("red") case Color.Green: print("green") case _ as unreachable: assert_never(unreachable)This will catch the error:
$ mypy main.py main.py:19: error: Argument 1 to "assert_never" has incompatible type "Literal[Color.Blue]"; expected "NoReturn"This boilerplate requires the programmer to:
- If running on Python <3.11, add
typing_extensionsto their dependencies - For each module they are using
match, importassert_never - For each
matchstatement, add thecase _clause
This hurts the ergonomics of trying to pervasively enforce this kind of correctness.
This also requires the programmer not to forget to add this boilerplate to every match clause, which can be error prone, especially for large projects wanting to guarantee correctness.
If using coverage, the boilerplate increases further as there will be no reasonable way to add tests to cover the extra unreachable cases. An extra # pragma: no cover comment must be added to every unreachable case:
def print_color(color: Color) -> None: match color: case Color.Red: print("red") case Color.Green: print("green") case _ as unreachable: # pragma: no cover assert_never(unreachable)Other Languages
Other languages with pattern matching, such as Rust, enforce exhaustive pattern matching, and it has been observed to have a positive effect on program correctness.