Skip to content

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

Closed
@Don-Burns

Description

@Don-Burns
Contributor

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

Activity

A5rocks

A5rocks commented on May 23, 2025

@A5rocks
Collaborator

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

added and removed
bugmypy got something wrong
on May 23, 2025
Don-Burns

Don-Burns commented on May 23, 2025

@Don-Burns
ContributorAuthor

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

A5rocks commented on May 23, 2025

@A5rocks
Collaborator

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

Don-Burns commented on May 23, 2025

@Don-Burns
ContributorAuthor

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

A5rocks commented on May 23, 2025

@A5rocks
Collaborator

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

sterliakov commented on May 23, 2025

@sterliakov
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

Don-Burns commented on May 23, 2025

@Don-Burns
ContributorAuthor

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

sterliakov commented on May 23, 2025

@sterliakov
Collaborator

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      Participants

      @A5rocks@sterliakov@Don-Burns

      Issue actions

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