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 file
So 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_extensions
to their dependencies - For each module they are using
match
, importassert_never
- For each
match
statement, 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 case
s. 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.
Activity
emmatyping commentedon Sep 4, 2022
I actually would prefer this to be on by default. The superseding PEP 634 says:
In addition, there are already so many flags and I think this is a case where it is reasonable to diverge from runtime.
hauntsaninja commentedon Sep 4, 2022
Note that mypy will already complain about code like the following, so this shouldn't be a hard check to add:
Take a look at #12267 if you're interested in implementing this.
For what it's worth:
I would certainly accept a disabled-by-default error code for this to mypy. I don't have enough experience with the match statement or sense of how it's used in the ecosystem to have an opinion on enabled-by-default or not.
hauntsaninja commentedon Sep 4, 2022
(Btw, a little off topic, but if you have open source code that makes interesting use of typing features, pattern matching, or has surfaced mypy usability issues for you previously, let me know, and I can add it to https://github.com/hauntsaninja/mypy_primer )
johnthagen commentedon Sep 4, 2022
@ethanhs I would too. I clarified the initial post to express this. I originally posted off-by-default to try to avoid controversy if the mypy team feels we shouldn't diverge from the runtime behavior. But I agree with everything you said above.
Add a new async iterable select() function
Add a new async iterable select() function
xmo-odoo commentedon Oct 16, 2023
I was also surprised that even in
--strict
mode mypy does not warn about this issue, my use case was processing a commands set so along the lines ofand the entire point of the union was the assumption that
match
would perform exhaustiveness checking, and would tell me if I forgot to process new commands. Sadly it's not to be.OCaml and Swift as well, Haskell -- or at least ghc -- stands out for not having exhaustive checking by default, and IIRC it's because the type system is sufficiently complex that it's a hard problem in that specific language, as well as the relevant GHC code being pretty old and crufty and not having a real champion / owner.
bskinn commentedon Dec 23, 2023
TypeScript also is equipped to evaluate its
[key in <type>]
construction exhaustively, I believe by default.The following shows a
Property '[E_Color.Blue]' is missing in type '{ red: string; green: string; }' but required in type '{ red: string; green: string; blue: string; }'
error in one of my projects:9999years commentedon Mar 26, 2024
I was pretty shocked to discover that this behavior isn't implemented. This error message is also not great:
13 remaining items