Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

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

Merged
merged 26 commits into from
Jun 4, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
02796d9
Add check for exhaustive match statements
Don-Burns May 23, 2025
3aaa98b
Add flag for exhaustive match statements
Don-Burns May 23, 2025
6b613f5
Update docs for exhaustive match statements
Don-Burns May 23, 2025
3341b4b
Add flag to mypy_primer
Don-Burns May 23, 2025
a186cc8
Revert "Add flag to mypy_primer"
Don-Burns May 24, 2025
ec55c81
Move tests to 3.10 file so only run on >=3.10
Don-Burns May 24, 2025
8ce5b6a
Rename flag to better follow other flags
Don-Burns May 24, 2025
3581ec9
Fix doc gen error
Don-Burns May 24, 2025
0f9ed6a
Set default True for CI run, will revert before merge
Don-Burns May 24, 2025
62e4089
Revert "Set default True for CI run, will revert before merge"
Don-Burns May 24, 2025
abf7e98
Add explicit error code - change [misc] -> [exhaustive-match]
Don-Burns May 24, 2025
6a525f7
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 24, 2025
7dc7c01
Add missed docs
Don-Burns May 24, 2025
3ab9848
fixup! Add missed docs
Don-Burns May 24, 2025
1a3e359
fixup! fixup! Add missed docs
Don-Burns May 24, 2025
fecd37d
Change to error code only for exhaustive match
Don-Burns May 24, 2025
525924f
Merge doc literal paragraph
Don-Burns May 26, 2025
f798b43
Empty commit to trigger CI
Don-Burns May 27, 2025
9b200ac
Apply suggestions to remove whitespace from tests
Don-Burns May 28, 2025
681a327
fixup! Apply suggestions to remove whitespace from tests
Don-Burns May 28, 2025
d219871
Add test for more complex narrowing use case
Don-Burns May 28, 2025
914bff4
fixup! Add test for more complex narrowing use case
Don-Burns May 28, 2025
4cc25fe
Merge branch 'master' into feat/exhaustive-match
Don-Burns May 28, 2025
da2752a
Change test for readability
Don-Burns Jun 3, 2025
3eaa5ad
error message change, use notes
hauntsaninja Jun 3, 2025
23afed2
tweak docs
hauntsaninja Jun 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Add check for exhaustive match statements
  • Loading branch information
Don-Burns committed May 23, 2025
commit 02796d944b0144535a7029e18947c49ca45c161b
7 changes: 7 additions & 0 deletions 7 mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -5452,6 +5452,7 @@ def visit_match_stmt(self, s: MatchStmt) -> None:
inferred_types = self.infer_variable_types_from_type_maps(type_maps)

# The second pass narrows down the types and type checks bodies.
unmatched_types: TypeMap = None
for p, g, b in zip(s.patterns, s.guards, s.bodies):
current_subject_type = self.expr_checker.narrow_type_from_binder(
named_subject, subject_type
Expand Down Expand Up @@ -5508,6 +5509,12 @@ 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
Contributor 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


if unmatched_types is not None:
# for expr, typ in unmatched_types.items():
for typ in set(unmatched_types.values()):
self.msg.match_statement_unexhaustive_match(typ, s)

# This is needed due to a quirk in frame_context. Without it types will stay narrowed
# after the match.
Expand Down
8 changes: 8 additions & 0 deletions 8 mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -2486,6 +2486,14 @@ def type_parameters_should_be_declared(self, undeclared: list[str], context: Con
code=codes.VALID_TYPE,
)

def match_statement_unexhaustive_match(self, typ: Type, context: Context) -> None:
type_str = format_type(typ, self.options)
msg = (
f"Cases within match statement do not exhaustively handle all values: {type_str}."
" If not intended to handle all cases, use `case _: pass`"
)
self.fail(msg, context)


def quote_type_string(type_string: str) -> str:
"""Quotes a type representation for use in messages."""
Expand Down
136 changes: 136 additions & 0 deletions 136 test-data/unit/check-match-exhaustive.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
[case testExhaustiveMatchNoFlag]
# flags: --python-version 3.12
a: int = 5
match a:
case 1:
pass
case _:
pass

b: str = "hello"
match b:
case "bye":
pass
case _:
pass

[case testNonExhaustiveMatchNoFlag]
# flags: --python-version 3.12
a: int = 5
match a:
case 1:
pass

b: str = "hello"
match b:
case "bye":
pass


[case testExhaustiveMatchWithFlag]
# flags: --python-version 3.12 --only-allow-exhaustive-match-statements
a: int = 5
match a:
case 1:
pass
case _:
pass

b: str = "hello"
match b:
case "bye":
pass
case _:
pass

[case testNonExhaustiveMatchWithFlag]
# flags: --python-version 3.12 --only-allow-exhaustive-match-statements
a: int = 5
match a: # E: Cases within match statement do not exhaustively handle all values: "int". If not intended to handle all cases, use `case _: pass`
case 1:
pass

b: str = "hello"
match b: # E: Cases within match statement do not exhaustively handle all values: "str". If not intended to handle all cases, use `case _: pass`
case "bye":
pass

[case testEnumNonExhaustiveWithFlag]
# flags: --python-version 3.12 --only-allow-exhaustive-match-statements

import enum

class Color(enum.Enum):
RED = 1
BLUE = 2
GREEN = 3

val: Color = Color.RED

match val: # E: Cases within match statement do not exhaustively handle all values: "Literal[Color.GREEN]". If not intended to handle all cases, use `case _: pass`
case Color.RED:
a = "red"
case Color.BLUE:
a= "blue"

[builtins fixtures/enum.pyi]


[case testEnumExhaustiveWithFlag]
# flags: --python-version 3.12 --only-allow-exhaustive-match-statements

import enum

class Color(enum.Enum):
RED = 1
BLUE = 2

val: Color = Color.RED

match val:
case Color.RED:
a = "red"
case Color.BLUE:
a= "blue"

[builtins fixtures/enum.pyi]

[case testEnumMultipleMissingMatchesWithFlag]
# flags: --python-version 3.12 --only-allow-exhaustive-match-statements

import enum

class Color(enum.Enum):
RED = 1
BLUE = 2
GREEN = 3

val: Color = Color.RED

match val: # E: Cases within match statement do not exhaustively handle all values: "Literal[Color.BLUE, Color.GREEN]". If not intended to handle all cases, use `case _: pass`
case Color.RED:
a = "red"


[builtins fixtures/enum.pyi]

[case testEnumFallbackWithFlag]
# flags: --python-version 3.12 --only-allow-exhaustive-match-statements

import enum

class Color(enum.Enum):
RED = 1
BLUE = 2
GREEN = 3

val: Color = Color.RED

match val:
case Color.RED:
a = "red"
case _:
a = "other"


[builtins fixtures/enum.pyi]
Morty Proxy This is a proxified and sanitized view of the page, visit original site.