Skip to content

Conversation

cdce8p
Copy link
Collaborator

@cdce8p cdce8p commented Oct 2, 2025

The rest type can't be inferred to be uninhabited if the inner pattern matched a Mapping or Sequence.

Fixes #19981
Fixes #19995

Copy link
Contributor

github-actions bot commented Oct 2, 2025

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

@cdce8p cdce8p requested a review from ilevkivskyi October 2, 2025 22:33
):
# Can't narrow rest type to uninhabited
# if narrowed_type is dict or list.
# Those can be matched by Mapping or Sequence patterns.
Copy link
Collaborator

@A5rocks A5rocks Oct 3, 2025

Choose a reason for hiding this comment

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

I... don't really follow. I don't know anything about the pattern matching checker, so I'm just going off inference/comments elsewhere, but isn't it right to say the rest is uninhabited? If I were to guess the problem is more with the | rather than the (_, {...})?

(sorry, you'll probably just have to explain why this is right when this comment does that. I just don't get it...)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The rest type is calculated from the intersection of the subject inner type and the matched inner type. Let's consider an example. Say the subject type is int | str and the matched type is int, the rest type will then be str.

For dict and list this is a bit more complicated as those are match patterns itself. So if the subject is {"a": 1, "b": 2} inferred to dict[str, int] and the case {"a": 2} the pattern would not actually match. However the inferred matched type is still dict[str, int] and the intersection would be Never which isn't correct. A similar issue happens with list. So we have to ignore the intersection rest type for Mapping and Sequence sub-patterns for those.

--
I'm not sure if it might even affect other generic classes as well. So far though I haven't been able to trigger the issue with any other examples so I'd leave it at these two for now.

Copy link
Collaborator

Choose a reason for hiding this comment

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

OK thanks that makes more sense. In that case I'm surprised about the check that rest is uninhabited! But more importantly, it seems like we already have machinery already doing this? (or did I misunderstand your explanation):

x: dict[str, int] = {"b": 0}

match x:
    case {"a": 5}:
        pass
    case b:
        reveal_type(b)  # N: Revealed type is "builtins.dict[builtins.str, builtins.int]"

Copy link
Collaborator Author

@cdce8p cdce8p Oct 3, 2025

Choose a reason for hiding this comment

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

But more importantly, it seems like we already have machinery already doing this? [...]

x: dict[str, int] = {"b": 0}

match x:
    case {"a": 5}:
        pass
    case b:
        reveal_type(b)  # N: Revealed type is "builtins.dict[builtins.str, builtins.int]"

The issue only happens for Mapping or Sequence patterns inside a Sequence pattern. Furthermore you'll need a wildcard match. With that mypy currently thinks "oh it's a wildcard so it always matches, thus the rest should be never". That's only true though if the parent sequence pattern itself matches, so we shouldn't infer rest in those cases.

Copy link
Collaborator

@A5rocks A5rocks Oct 3, 2025

Choose a reason for hiding this comment

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

I see. Is there a reason why this doesn't narrow the 2nd tuple element to Never?: (in comparison to if x is a dict, which I can see now and is I think what you are talking about)

x: str = "blah"

match (x, 4):
    case ("a", _):
        pass
    case b:
        reveal_type(b)  # N: Revealed type is "tuple[builtins.str, Literal[4]?]"

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The rest type for x is inferred as str whereas with the dict example it's inferred as Never since technically both the subject and inner match types are identical dict[str, str].

m7: dict[str, str]

match (m7, m7):
    case ({"a": "1"}, _):
        ...
    case (_, {"a": "2"}):
        ...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
topic-match-statement Python 3.10's match statement topic-reachability Detecting unreachable code
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Incorrect "Alternative patterns bind different names" with mapping capture statements Unreachable false positive in match statement with tuples
2 participants