mypy icon indicating copy to clipboard operation
mypy copied to clipboard

Do not treat `match` value patterns as `isinstance` checks

Open sterliakov opened this issue 2 months ago • 8 comments

Fixes #20358.

As discovered by @randolf-scholz in #20142, mypy treats value patterns in match statement in a completely wrong way.

From PEP 622:

Literal pattern uses equality with literal on the right hand side, so that in the above example number == 0 and then possibly number == 1, etc will be evaluated.

Existing tests for the feature are invalid: for example,

foo: object

match foo:
    case 1:
        reveal_type(foo)

must reveal object, and test testMatchValuePatternNarrows asserts the opposite. Here's a runtime example:

>>> class A:
...     def __eq__(self,o): return True
...     
>>> match A():
...     case 1:
...         print("eq")
...         
eq

I have updated the existing tests accordingly.

The idea is that value patterns are essentially equivalent to if foo == SomeValue checks, not isinstance checks modelled by conditional_types_wit_intersection.

The original implementation was introduced in #10191.

sterliakov avatar Oct 30 '25 14:10 sterliakov

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

github-actions[bot] avatar Oct 30 '25 14:10 github-actions[bot]

I'm frankly not sure what part of the core team to ping here - reviewers of the original PR, @JukkaL? @randolf-scholz please also take a look if you have time, IIRC some recent PRs of yours were related to pattern matching?

Primer finds nothing, apparently match isn't that popular in wild...

sterliakov avatar Oct 30 '25 14:10 sterliakov

Primer finds nothing, apparently match isn't that popular in wild...

Well, many popular projects still support 3.9, but match-case requires at least 3.10, so that's not surprising.

randolf-scholz avatar Oct 30 '25 14:10 randolf-scholz

One thing that might be worth testing is if it interacts correctly with unions types and union patterns, e.g.

from typing import Literal

def test1(x: Literal[1,2,3]) -> None:
    match x:
        case 1:
            reveal_type(x)
        case other:
            reveal_type(x)

def test2(x: Literal[1,2,3]) -> None:
    match x:
        case 1:
            reveal_type(x)
        case 2:
            reveal_type(x)
        case 3:
            reveal_type(x)
        case other:
            assert_never(x)

def test3(x: Literal[1,2,3]) -> None:
    match x:
        case 1 | 3:
            reveal_type(x)
        case other:
            reveal_type(x)

and possibly the same with enum arguments / enum patterns.

randolf-scholz avatar Oct 30 '25 14:10 randolf-scholz

I don't want to add any enum tests here, they won't be representative due to #19594 - I'd prefer to add match equivalents to tests there once this PR is merged. Unions are more relevant, thanks!

sterliakov avatar Oct 30 '25 15:10 sterliakov

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

github-actions[bot] avatar Oct 30 '25 22:10 github-actions[bot]

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

github-actions[bot] avatar Nov 24 '25 12:11 github-actions[bot]

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

github-actions[bot] avatar Dec 03 '25 15:12 github-actions[bot]