Skip to content

mypy not raising error on inexhaustive case/match statements in strict mode #19136

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
Don-Burns opened this issue May 23, 2025 · 8 comments · May be fixed by #19144
Open

mypy not raising error on inexhaustive case/match statements in strict mode #19136

Don-Burns opened this issue May 23, 2025 · 8 comments · May be fixed by #19144
Labels
feature topic-match-statement Python 3.10's match statement

Comments

@Don-Burns
Copy link

Bug Report

(A clear and concise description of what the bug is.)

To Reproduce
Gist: https://mypy-play.net/?mypy=latest&python=3.12&gist=61f3db46cdaf7782c6679322e8809f47&flags=strict%2Ccheck-untyped-defs

import enum
import random


class Test(enum.Enum):
    A = 1
    B = 2


val = list(Test)[random.randint(0, 2)]
reveal_type(val)
match val:
    case Test.A:
        print("A")
    # case Test.B:
    #     print("B")

val2 = random.randint(0, 2)
reveal_type(val2)
match val2:
    case 1:
        print("1")

Expected Behavior

Expected both of the match statements to raise errors due to not exhaustively matching on the enum/int values

Actual Behavior

File is passed as ok when it should raise errors
Pyright is able to catch both cases in strict mode as expected in case this helps: pyright playground link

Your Environment

  • Mypy version used: tested on 1.15.0, 1.14.1, 1.10.1
  • Mypy command-line flags: --strict
  • Python version used: 3.12 & 3.10

Possibly Related Issues?
Below are the most relevant issues I could spot, but don't seem to quite align with the case here to the best of my understanding

@Don-Burns Don-Burns added the bug mypy got something wrong label May 23, 2025
@A5rocks
Copy link
Collaborator

A5rocks commented May 23, 2025

mypy doesn't have this feature, you need to add case _: typing.assert_never(val) to get exhaustiveness checking.

@A5rocks A5rocks added feature and removed bug mypy got something wrong labels May 23, 2025
@Don-Burns
Copy link
Author

Ah apologies then, I could have sworn I had seen this working in the past, but must have just been from my IDE's checking rather than running mypy (VsCode with pylance for IDE)
Would be a great feature though! Understand if this gets closed though

@A5rocks
Copy link
Collaborator

A5rocks commented May 23, 2025

Yeah maybe opt-in (what mypy currently has) is worse than opt-out (what pyright currently has) for strict mode at least. But I'm not too good with knowing what features mypy should add!

@Don-Burns
Copy link
Author

If the maintainers are open to it, I can try to take a stab at implementing this, but admittedly I am not very familiar with mypy's code base to know how hard/easy this may be to tackle.

@A5rocks
Copy link
Collaborator

A5rocks commented May 23, 2025

Implementation wise, I think this is simple: at the end of this loop:

for p, g, b in zip(s.patterns, s.guards, s.bodies):

check whether mypy is in strict mode (maybe add a new mode?) and whether the narrowed type (copy how current_subject_type is done, I think?) is UninhabitedType.

@sterliakov
Copy link
Collaborator

It should be simple to add, but I don't think it's really a sensible check to enable globally. Python's match statement is not as good as Rust's (mostly because it's a statement and not a statement, so has no "return value"). Enforcing that match is always exhaustive is equivalent to requiring that any if has a corresponding else, which isn't something commonly enforced in python code. There's a simple case other: assert_never(other) option to enforce exhaustiveness of every match stmt separately.

@Don-Burns
Copy link
Author

I agree partly here, while not quite as powerful as some other languages like Rust's or Scala's match statements, there is still a lot of power in python's version. For me one great benefit of being able to turn this on would be to flag any areas that were previously thought to be exhaustive or the dev thought they were exhaustive and didn't realise otherwise. If a new pattern becomes possible and there isn't a defined path, it feels like a code smell to me. Having an option at least in mypy to catch this would be great IMO.

You can still "opt-out" if no match causing no effect is the intended behaviour with something like case _: pass in the final clause. I can totally see how this comes down to which scenario you want to be explicit in though.

@sterliakov
Copy link
Collaborator

sterliakov commented May 23, 2025

Hm, case_: pass is a good explicit opt-out, quite in line with _ => {} in Rust to express "yes, it's non-exhaustive, I know, stfu". That's a good argument in favour of this proposal. It's especially nice because exhaustive matches should still occur more often than non-exhaustive, and case _: pass is shorter, needs no extra imports and is more immediately obvious compared to case other: assert_never(other). And match predates assert_never by one version, so that import may not even come from stdlib typing.

I'm +1 on adding this check, except for one problem: it shouldn't be on by default (and even in strict mode IMO), and everything else is seldom ever enabled by users, be it a config flag or --enable-error-code.

@sterliakov sterliakov added the topic-match-statement Python 3.10's match statement label May 23, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature topic-match-statement Python 3.10's match statement
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants