Skip to content

Add flag to raise error if match statement does not match exaustively #19144

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
wants to merge 23 commits into
base: master
Choose a base branch
from

Conversation

Don-Burns
Copy link

Fixes #19136

Change is to add a mode to catch when a match statement is not handling all cases exhaustively, similar to what pyright does by default.
After discussion on #19136 I put it behind a new flag that is not enabled by default.
I updated docs to include information on the new flag also.

Please let me know if anything is not following standards, in particular I wasn't sure what to name this new flag to be descriptive while following existing flag naming style.

@A5rocks
Copy link
Collaborator

A5rocks commented May 23, 2025

  1. could you enable it by default for mypy primer's output
  2. IMO it would be neater to put this behind a new error code instead. Though those are even less discoverable than new modes so...

This comment has been minimized.

Add flag to primer as asked in PR
@Don-Burns
Copy link
Author

  1. could you enable it by default for mypy primer's output
  2. IMO it would be neater to put this behind a new error code instead. Though those are even less discoverable than new modes so...

I added where it looked like made sense to pass the arg to mypy_primer (workflow files?), let me know if I misunderstood where to pass this.

I'll have a look at the error code option when I have a chance and see how it feels. Should still be possible to enable discovery, but right now at least I think CLI mention in --help would be ideal so users don't need the docs unless they are really deep diving. Not sure if there is some middle ground in this regard.

@A5rocks
Copy link
Collaborator

A5rocks commented May 23, 2025

Sorry I meant specifically just default to enabled for a commit and let mypy primer run.

Anyways, this should already work for an error code, I just don't know how to disable them by default. (The fact it would be a smaller change is why I said it would be neater)

This comment has been minimized.

Don-Burns added 4 commits May 24, 2025 08:39
There appears to be a parsing issue in sphyx? For the lines with `match val: # error: ...` the `: # error:` part is being interpreted as a directive I think? Means the # is stripped from the python code and the docs can't build

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

@Don-Burns
Copy link
Author

Sorry I meant specifically just default to enabled for a commit and let mypy primer run.

CI run with default set to True
commit: 0f9ed6a
mypy_primer comment: #19144 (comment)

This comment has been minimized.

@Don-Burns Don-Burns marked this pull request as ready for review May 24, 2025 15:16

This comment has been minimized.

@Don-Burns
Copy link
Author

Test passing now.

My preference is for the CLI arg for discoverability in cli help etc.
But if using an error code only to enable via config is preferred, this is what that would look like from what I can tell: https://github.com/python/mypy/compare/master...Don-Burns:mypy:feat/exhaustive-match-error-code-approach?expand=1

Copy link
Collaborator

@A5rocks A5rocks left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implementation looks good to me, hopefully this is fine as a feature for others!

@@ -612,3 +612,51 @@ Example:
# mypy: disallow-any-explicit
from typing import Any
x: Any = 1 # Error: Explicit "Any" type annotation [explicit-any]


.. _code-exhaustive-match:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if the others have duplicated docs but if not could you just link from one of these sections to the other?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on feedback below, I moved to only be error code based.
Means the duplication in the CLI command doc page is removed.

@@ -469,6 +469,11 @@ If we forget to handle one of the cases, mypy will generate an error:

Exhaustiveness checking is also supported for match statements (Python 3.10 and later).
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this could be expanded instead of adding a new paragraph:

Suggested change
Exhaustiveness checking is also supported for match statements (Python 3.10 and later).
Exhaustiveness checking is also supported for match statements (Python 3.10 and later). With
:option:`--disallow-inexhaustive-match-statements <mypy --disallow-inexhaustive-match-statements>`,
mypy will even warn if match statements are inexhaustive.

(not sure that's better than your current approach now that I write it out, actually)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you are right here 👍
Changed to 1 paragraph

@A5rocks
Copy link
Collaborator

A5rocks commented May 25, 2025

Copying mypy-primer comment for ease of viewing:

cki-lib (https://gitlab.com/cki-project/cki-lib)
+ cki_lib/inttests/remote_responses.py:187: error: Cases within match statement do not exhaustively handle all values: "str". If not intended to handle all cases, use `case _: pass`  [misc]

pandas (https://github.com/pandas-dev/pandas)
+ pandas/core/computation/parsing.py:170: error: Cases within match statement do not exhaustively handle all values: "str". If not intended to handle all cases, use `case _: pass`  [misc]

pylox (https://github.com/sco1/pylox)
+ pylox/parser.py:106: error: Cases within match statement do not exhaustively handle all values: "Literal[TokenType.LEFT_PAREN, TokenType.RIGHT_PAREN, TokenType.LEFT_BRACE, TokenType.RIGHT_BRACE, TokenType.COMMA, TokenType.DOT, TokenType.MINUS, TokenType.PLUS, TokenType.SEMICOLON, TokenType.SLASH, TokenType.STAR, TokenType.CARAT, TokenType.PERCENT, TokenType.BACK_SLASH, TokenType.BANG, TokenType.BANG_EQUAL, TokenType.EQUAL, TokenType.EQUAL_EQUAL, TokenType.GREATER, TokenType.GREATER_EQUAL, TokenType.LESS, TokenType.LESS_EQUAL, TokenType.IDENTIFIER, TokenType.STRING, TokenType.NUMBER, TokenType.AND, TokenType.BREAK, TokenType.CONTINUE, TokenType.ELSE, TokenType.FALSE, TokenType.INCLUDE, TokenType.NIL, TokenType.OR, TokenType.SUPER, TokenType.THIS, TokenType.TRUE, TokenType.EOF]". If not intended to handle all cases, use `case _: pass`  [misc]

steam.py (https://github.com/Gobot1234/steam.py)
+ steam/id.py:326: error: Cases within match statement do not exhaustively handle all values: "int | Never". If not intended to handle all cases, use `case _: pass`  [misc]
+ steam/id.py:431: error: Cases within match statement do not exhaustively handle all values: "int | Never". If not intended to handle all cases, use `case _: pass`  [misc]
+ steam/id.py:484: error: Cases within match statement do not exhaustively handle all values: "Any | Any". If not intended to handle all cases, use `case _: pass`  [misc]
+ steam/manifest.py:380: error: Cases within match statement do not exhaustively handle all values: "str". If not intended to handle all cases, use `case _: pass`  [misc]
+ steam/state.py:397: error: Cases within match statement do not exhaustively handle all values: "Any | Any". If not intended to handle all cases, use `case _: pass`  [misc]
+ steam/state.py:397: error: Cases within match statement do not exhaustively handle all values: "int | Never". If not intended to handle all cases, use `case _: pass`  [misc]
+ steam/state.py:1196: error: Cases within match statement do not exhaustively handle all values: "int | EChatRoomMemberStateChange". If not intended to handle all cases, use `case _: pass`  [misc]
+ steam/state.py:1224: error: Cases within match statement do not exhaustively handle all values: "int | EChatRoomMemberStateChange". If not intended to handle all cases, use `case _: pass`  [misc]
+ steam/state.py:1432: error: Cases within match statement do not exhaustively handle all values: "Any | Any". If not intended to handle all cases, use `case _: pass`  [misc]
+ steam/state.py:1432: error: Cases within match statement do not exhaustively handle all values: "int | EChatRoomServerMessage". If not intended to handle all cases, use `case _: pass`  [misc]
+ steam/state.py:2358: error: Cases within match statement do not exhaustively handle all values: "int". If not intended to handle all cases, use `case _: pass`  [misc]
+ steam/ext/commands/cooldown.py:60: error: Cases within match statement do not exhaustively handle all values: "int | Never". If not intended to handle all cases, use `case _: pass`  [misc]

core (https://github.com/home-assistant/core)
+ homeassistant/components/recorder/statistics.py:1052: error: Cases within match statement do not exhaustively handle all values: "Literal[StatisticMeanType.NONE, StatisticMeanType.ARITHMETIC]". If not intended to handle all cases, use `case _: pass`  [misc]
+ homeassistant/components/recorder/statistics.py:1301: error: Cases within match statement do not exhaustively handle all values: "Literal[StatisticMeanType.NONE]". If not intended to handle all cases, use `case _: pass`  [misc]
+ homeassistant/components/recorder/statistics.py:1321: error: Cases within match statement do not exhaustively handle all values: "Literal[StatisticMeanType.NONE]". If not intended to handle all cases, use `case _: pass`  [misc]
+ homeassistant/components/recorder/statistics.py:1397: error: Cases within match statement do not exhaustively handle all values: "Literal[StatisticMeanType.NONE]". If not intended to handle all cases, use `case _: pass`  [misc]
+ homeassistant/components/onkyo/media_player.py:112: error: Cases within match statement do not exhaustively handle all values: "str". If not intended to handle all cases, use `case _: pass`  [misc]
+ homeassistant/components/homekit_controller/utils.py:26: error: Cases within match statement do not exhaustively handle all values: "list[str]". If not intended to handle all cases, use `case _: pass`  [misc]
+ homeassistant/components/home_connect/coordinator.py:191: error: Cases within match statement do not exhaustively handle all values: "Any | Any". If not intended to handle all cases, use `case _: pass`  [misc]
+ homeassistant/components/go2rtc/__init__.py:265: error: Cases within match statement do not exhaustively handle all values: "Any". If not intended to handle all cases, use `case _: pass`  [misc]
+ homeassistant/components/knx/project.py:142: error: Cases within match statement do not exhaustively handle all values: "Any | Any". If not intended to handle all cases, use `case _: pass`  [misc]
+ homeassistant/components/knx/expose.py:211: error: Cases within match statement do not exhaustively handle all values: "str | Any". If not intended to handle all cases, use `case _: pass`  [misc]
+ homeassistant/components/bryant_evolution/climate.py:168: error: Cases within match statement do not exhaustively handle all values: "Any". If not intended to handle all cases, use `case _: pass`  [misc]
+ homeassistant/components/sensor/recorder.py:620: error: Cases within match statement do not exhaustively handle all values: "Literal[StatisticMeanType.NONE]". If not intended to handle all cases, use `case _: pass`  [misc]
+ homeassistant/components/miele/switch.py:136: error: Cases within match statement do not exhaustively handle all values: "str". If not intended to handle all cases, use `case _: pass`  [misc]
+ homeassistant/components/home_connect/__init__.py:115: error: Cases within match statement do not exhaustively handle all values: "int". If not intended to handle all cases, use `case _: pass`  [misc]
+ homeassistant/components/eheimdigital/climate.py:107: error: Cases within match statement do not exhaustively handle all values: "Literal[HVACMode.HEAT, HVACMode.COOL, HVACMode.HEAT_COOL, HVACMode.DRY, HVACMode.FAN_ONLY]". If not intended to handle all cases, use `case _: pass`  [misc]
+ homeassistant/components/eheimdigital/climate.py:138: error: Cases within match statement do not exhaustively handle all values: "Any | Any". If not intended to handle all cases, use `case _: pass`  [misc]
+ homeassistant/components/qbus/config_flow.py:49: error: Cases within match statement do not exhaustively handle all values: "Any | Any". If not intended to handle all cases, use `case _: pass`  [misc]
+ homeassistant/components/home_connect/light.py:138: error: Cases within match statement do not exhaustively handle all values: "tuple[Never, Never]". If not intended to handle all cases, use `case _: pass`  [misc]
+ homeassistant/components/eq3btsmart/climate.py:240: error: Cases within match statement do not exhaustively handle all values: "str". If not intended to handle all cases, use `case _: pass`  [misc]

jax (https://github.com/google/jax)
+ jax/_src/lax/lax.py:5819: error: Cases within match statement do not exhaustively handle all values: "Any | Any". If not intended to handle all cases, use `case _: pass`  [misc]
+ jax/_src/lax/lax.py:6034: error: Cases within match statement do not exhaustively handle all values: "Any | Any". If not intended to handle all cases, use `case _: pass`  [misc]
+ jax/_src/lax/lax.py:6057: error: Cases within match statement do not exhaustively handle all values: "Any | Any". If not intended to handle all cases, use `case _: pass`  [misc]
+ jax/_src/pallas/core.py:562: error: Cases within match statement do not exhaustively handle all values: "Any". If not intended to handle all cases, use `case _: pass`  [misc]
+ jax/_src/pallas/mosaic/verification.py:382: error: Cases within match statement do not exhaustively handle all values: "Any | Any". If not intended to handle all cases, use `case _: pass`  [misc]
+ jax/_src/pallas/mosaic/pallas_call_registration.py:80: error: Cases within match statement do not exhaustively handle all values: "Any". If not intended to handle all cases, use `case _: pass`  [misc]
+ jax/_src/pallas/mosaic_gpu/core.py:213: error: Cases within match statement do not exhaustively handle all values: "Any". If not intended to handle all cases, use `case _: pass`  [misc]

bokeh (https://github.com/bokeh/bokeh)
+ src/bokeh/core/serialization.py: note: In function "_encode_typed_array":
+ src/bokeh/core/serialization.py:395:13: error: Cases within match statement do not exhaustively handle all values: "Literal['u']". If not intended to handle all cases, use `case _: pass`  [misc]
+ src/bokeh/core/serialization.py:401:21: error: Cases within match statement do not exhaustively handle all values: "int". If not intended to handle all cases, use `case _: pass`  [misc]
+ src/bokeh/core/serialization.py:407:21: error: Cases within match statement do not exhaustively handle all values: "int". If not intended to handle all cases, use `case _: pass`  [misc]
+ src/bokeh/core/serialization.py: note: At top level:

archinstall (https://github.com/archlinux/archinstall)
+ archinstall/tui/curses_menu.py:79: error: Cases within match statement do not exhaustively handle all values: "Literal[ResultType.Skip, ResultType.Reset]". If not intended to handle all cases, use `case _: pass`  [misc]
+ archinstall/tui/curses_menu.py:912: error: Cases within match statement do not exhaustively handle all values: "Literal[PreviewStyle.NONE]". If not intended to handle all cases, use `case _: pass`  [misc]
+ archinstall/tui/curses_menu.py:925: error: Cases within match statement do not exhaustively handle all values: "Literal[PreviewStyle.NONE]". If not intended to handle all cases, use `case _: pass`  [misc]
+ archinstall/lib/menu/abstract_menu.py:108: error: Cases within match statement do not exhaustively handle all values: "Literal[ResultType.Skip]". If not intended to handle all cases, use `case _: pass`  [misc]
+ archinstall/lib/models/device_model.py:246: error: Cases within match statement do not exhaustively handle all values: "Literal[Unit.B, Unit.kB, Unit.MB, Unit.GB, Unit.TB, Unit.PB, Unit.EB, Unit.ZB, Unit.YB, Unit.KiB, Unit.MiB, Unit.GiB, Unit.TiB, Unit.PiB, Unit.EiB, Unit.ZiB, Unit.YiB]". If not intended to handle all cases, use `case _: pass`  [misc]
+ archinstall/lib/models/audio_configuration.py:41: error: Cases within match statement do not exhaustively handle all values: "Literal[Audio.NO_AUDIO]". If not intended to handle all cases, use `case _: pass`  [misc]
+ archinstall/lib/mirrors.py:200: error: Cases within match statement do not exhaustively handle all values: "Literal[ResultType.Reset]". If not intended to handle all cases, use `case _: pass`  [misc]
+ archinstall/lib/mirrors.py:329: error: Cases within match statement do not exhaustively handle all values: "Any | Any". If not intended to handle all cases, use `case _: pass`  [misc]
+ archinstall/lib/installer.py:400: error: Cases within match statement do not exhaustively handle all values: "Literal[EncryptionType.NoEncryption]". If not intended to handle all cases, use `case _: pass`  [misc]
+ archinstall/lib/installer.py:1002: error: Cases within match statement do not exhaustively handle all values: "Literal[EncryptionType.Luks]". If not intended to handle all cases, use `case _: pass`  [misc]
+ archinstall/lib/configuration.py:128: error: Cases within match statement do not exhaustively handle all values: "Any | Any". If not intended to handle all cases, use `case _: pass`  [misc]
+ archinstall/lib/configuration.py:128: error: Cases within match statement do not exhaustively handle all values: "Any | None". If not intended to handle all cases, use `case _: pass`  [misc]
+ archinstall/lib/configuration.py:206: error: Cases within match statement do not exhaustively handle all values: "Literal[ResultType.Skip, ResultType.Reset]". If not intended to handle all cases, use `case _: pass`  [misc]
+ archinstall/lib/configuration.py:228: error: Cases within match statement do not exhaustively handle all values: "Literal[ResultType.Skip, ResultType.Reset]". If not intended to handle all cases, use `case _: pass`  [misc]
+ archinstall/lib/configuration.py:239: error: Cases within match statement do not exhaustively handle all values: "str". If not intended to handle all cases, use `case _: pass`  [misc]
+ archinstall/lib/profile/profile_menu.py:229: error: Cases within match statement do not exhaustively handle all values: "<subclass of "archinstall.default_profiles.profile.SelectResult" and "enum.auto"> | SelectResult". If not intended to handle all cases, use `case _: pass`  [misc]
+ archinstall/lib/disk/partitioning_menu.py:278: error: Cases within match statement do not exhaustively handle all values: "str". If not intended to handle all cases, use `case _: pass`  [misc]
+ archinstall/lib/disk/partitioning_menu.py:292: error: Cases within match statement do not exhaustively handle all values: "str". If not intended to handle all cases, use `case _: pass`  [misc]
+ archinstall/lib/disk/partitioning_menu.py:500: error: Cases within match statement do not exhaustively handle all values: "Literal[ResultType.Reset]". If not intended to handle all cases, use `case _: pass`  [misc]

Expression (https://github.com/cognitedata/Expression)
+ expression/core/tagged_union.py:67: error: Cases within match statement do not exhaustively handle all values: "tuple[Never, Never]". If not intended to handle all cases, use `case _: pass`  [misc]
+ expression/extra/parser.py:130: error: Cases within match statement do not exhaustively handle all values: "Result[tuple[_A, tuple[str, int]], str]". If not intended to handle all cases, use `case _: pass`  [misc]
+ expression/extra/parser.py:136: error: Cases within match statement do not exhaustively handle all values: "Result[tuple[_B, tuple[str, int]], str]". If not intended to handle all cases, use `case _: pass`  [misc]
+ tests/test_tagged_union.py:264: error: Cases within match statement do not exhaustively handle all values: "Any". If not intended to handle all cases, use `case _: pass`  [misc]

mitmproxy (https://github.com/mitmproxy/mitmproxy)
+ mitmproxy/net/http/validate.py:79: error: Cases within match statement do not exhaustively handle all values: "bytes". If not intended to handle all cases, use `case _: pass`  [misc]

openlibrary (https://github.com/internetarchive/openlibrary)
+ openlibrary/utils/isbn.py: note: In function "get_isbn_10s_and_13s":
+ openlibrary/utils/isbn.py:141: error: Cases within match statement do not exhaustively handle all values: "int". If not intended to handle all cases, use `case _: pass`  [misc]

Copy link
Collaborator

@hauntsaninja hauntsaninja left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! This is a pretty popular feature request: #13597

+1 on the error code suggestion. Mypy has been moving towards using error codes for changes like this. We can improve discoverability separately

@Don-Burns
Copy link
Author

Thanks! This is a pretty popular feature request: #13597

+1 on the error code suggestion. Mypy has been moving towards using error codes for changes like this. We can improve discoverability separately

Thanks for the feedback. I have merged the change to only be error code based into this PR

@Don-Burns
Copy link
Author

2 CI tasks failed on the github side, is it possible to retrigger them?

@A5rocks
Copy link
Collaborator

A5rocks commented May 26, 2025

You could push an empty commit

@Don-Burns
Copy link
Author

You could push an empty commit

Fair I forgot whole branch will be squashed so no need to worry about history here 😅

This comment has been minimized.

@Don-Burns Don-Burns requested a review from hauntsaninja May 27, 2025 08:49
@@ -5508,6 +5509,11 @@ def visit_match_stmt(self, s: MatchStmt) -> None:
else:
self.accept(b)
self.push_type_map(else_map, from_assignment=False)
unmatched_types = else_map
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it work as intended when typemaps contain more than immediate targets?

For example, what happens in the following fork of an existing test testMatchNarrowingUnionTypedDictViaIndex with this flag enabled? If I remember the surrounding logic correctly, there will be two typemap entries - for d['tag'] and for d, so two diagnostics?

from typing import Literal, TypedDict

class A(TypedDict):
    tag: Literal["a"]
    name: str

class B(TypedDict):
    tag: Literal["b"]
    num: int

d: A | B
match d["tag"]:
    case "a":
        reveal_type(d)  # N: Revealed type is "TypedDict('__main__.A', {'tag': Literal['a'], 'name': builtins.str})"
        reveal_type(d["name"])  # N: Revealed type is "builtins.str"

Copy link
Author

@Don-Burns Don-Burns May 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say that yes, it is behaving as I originally intended with 2 diagnostics. Added a test testExhaustiveMatchNarrowingUnionTypedDictViaIndex for the use case you have above.
d219871

My thinking of having multiple diagnostics is it might be a bit easier to read/understand in the case of longer unions, but I don't have a very strong opinion here either way

This comment has been minimized.

Fix for more deterministic behaviour

This comment has been minimized.

Copy link
Contributor

According to mypy_primer, this change doesn't affect type check results on a corpus of open source code. ✅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

mypy not raising error on inexhaustive case/match statements in strict mode
4 participants