Skip to content
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

Structural pattern matching is having an issue narrowing the type of tuple elements when they are unions #5957

Closed
willfrey opened this issue Jun 3, 2024 · 1 comment
Assignees
Labels
needs repro Issue has not been reproduced yet

Comments

@willfrey
Copy link

willfrey commented Jun 3, 2024

Environment data

  • Language Server version: 2024.5.1
  • OS and version: darwin arm64
  • Python version (and distribution if applicable, e.g. Anaconda): Python 3.12.3
  • python.analysis.indexing: true
  • python.analysis.typeCheckingMode: standard

Code Snippet

import typing

type Maybe[T] = T | None
type SparsePair[T, S] = tuple[Maybe[T], Maybe[S]]


def maybe_add(sparse_pair: SparsePair[int, int]) -> int:
    match sparse_pair:
        case None, None:
            return 0
        case a, None:
            # Pylance: "assert_type" mismatch: expected "int" but received "int | None"
            typing.assert_type(a, int)
            return a
        case None, b:
            # No problem in this case!
            typing.assert_type(b, int)
            return b
        case a, b:
            # Also okay here, too!
            typing.assert_type(a, int)
            typing.assert_type(b, int)
            return a + b
        case _:
            typing.assert_never(sparse_pair)

Expected behavior

I expect the typing.assert_type(a, int) statement to not raise any sort of error with Pylance/Pyright.

Actual behavior

Pylance/Pyright thinks that a has type int | None in the case a, None: ... block.

And for what it's worth, you can switch the order of the case a, None: ... and case None, b: ... and observe that the issue is only ever with the first case statement after the case None, None: ....

I realize that I could just use case int(a), None: ... or case None, int(b): ... but I'd like this to work when I otherwise cannot use a concrete type to use in the case statement to match against, like if the sparse pair was generic still (so, each element has type T | None for some generic T.

Additionally, I experimented with how Mypy handles this and Mypy does not seem to correctly narrow the types when matching against tuples like this at all, so this behavior is beyond what Mypy guarantees—which I appreciate!

Related Mypy issue: python/mypy#12364

Thank you!

@github-actions github-actions bot added the needs repro Issue has not been reproduced yet label Jun 3, 2024
@erictraut
Copy link
Contributor

This is by design. Pyright (the type checker upon which pylance is built) performs type narrowing for tuples only in the case where one tuple element can be narrowed. It doesn't perform generalized tuple expansion for all combinations of possible tuples. Such behavior would lead to a combinatoric explosion. For example, the type tuple[int | None, int | None] would need to be expanded to tuple[int, int] | tuple[int, None] | tuple[None, int] | tuple[None | None]. This isn't too bad for a two-element tuple, but it grows exponentially with the size of the tuple.

See this issue for more details.

As you point out, mypy doesn't perform any tuple expansion in this case, even when a single entry's type can be narrowed.

@rchiodo rchiodo closed this as completed Jun 3, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs repro Issue has not been reproduced yet
Projects
None yet
Development

No branches or pull requests

4 participants