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

Match statement with tuple of union type fails #15426

Open
charel-felten-rq opened this issue Jun 13, 2023 · 4 comments
Open

Match statement with tuple of union type fails #15426

charel-felten-rq opened this issue Jun 13, 2023 · 4 comments
Labels
bug mypy got something wrong topic-match-statement Python 3.10's match statement topic-type-narrowing Conditional type narrowing / binder

Comments

@charel-felten-rq
Copy link

Bug Report

I am typechecking a match statement with a tuple of union types and it fails.

To Reproduce

Playground: https://mypy-play.net/?mypy=latest&python=3.10&gist=992b588ec7e4807f3105db45fa9ef0a8

My union type:

TransactionType = str | int | None

(Problem persists even if I do:

from typing import Union
TransactionType = Union[str | int | None]

)

This works fine:

def test(a: TransactionType) -> bool:
    match a:
        case str():
            a.lower()
            return True
        case _:
            return False

This causes an error:

def test2(a: TransactionType, b: TransactionType) -> bool:
    match a,b:
        case str(), _:
            a.lower()
            return True
        case _:
            return False

Actual Behavior

main.py:13: error: Item "int" of "Union[str, int, None]" has no attribute "lower"  [union-attr]
main.py:13: error: Item "None" of "Union[str, int, None]" has no attribute "lower"  [union-attr]
Found 2 errors in 1 file (checked 1 source file)

Your Environment

  • Mypy version used: 1.3.0
  • Mypy command-line flags: None
  • Mypy configuration options from mypy.ini (and other config files): None
  • Python version used: 3.10 (also tested on 3.11, same problem)
@charel-felten-rq charel-felten-rq added the bug mypy got something wrong label Jun 13, 2023
@AlexWaygood AlexWaygood added topic-match-statement Python 3.10's match statement topic-type-narrowing Conditional type narrowing / binder labels Jun 13, 2023
@tmke8
Copy link
Contributor

tmke8 commented Jun 13, 2023

Related: #12364

@erictraut
Copy link

I think this is by design. This should probably be considered an enhancement request, not a bug.

When mypy encounters a match statement, it evaluates the type of the subject expression. In this case of test2 above, the subject expression is a, b, and its type is tuple[TransactionType, TransactionType]. It then "narrows" the subject type based on the individual case statements. This gets tricky in the case of tuples. For example, let's say that your first case statement pattern was str(), str(). That matches only if both elements in the tuple are str. What is the narrowed type in the fall-through (negative) case then? It would need to be a union of tuple types — tuple[int | None, TransactionType] | tuple[TransactionType, int | None] | tuple[int | None, int | None]. This is an unusual form of type narrowing in the sense that the type becomes more "complex" after narrowing. The number of subtypes can explode combinatorially as the size of the tuple grows. It's not surprising that a type checker would not handle this case.

I recently added support in pyright for the specific case where narrowing affects only one element of a tuple and leaves the remaining elements unaffected. Your test2 example meets this criteria, which means that your sample now type checks without errors in pyright. The narrowed subject type after the fall-through from the first case statement is tuple[int | None, TransactionType] because only the first tuple element was affected by the pattern.

Mypy maintainers might want to implement a similar strategy here, since I've found this case to be quite common. It's relatively easy to implement, and it doesn't suffer from combinatoric explosions.

@mishamsk
Copy link

I'd vote for this as well. Surprised to find this limitation today. My case is something like this:

from enum import Enum, auto

class SomeEnum(Enum):
    A = auto()
    B = auto()

class Base:
    pass

class Foo(Base):
    pass

class Bar(Base):
    pass

tag, value = SomeEnum.A, Foo()

match tag, value:
    case SomeEnum.A, Foo():
        print("Want to handle this case")
    case SomeEnum.B, Bar():
        print("This one too")
    case _:
        raise NotImplementedError(f"Can't handle {tag} with {value}")

so I do not really care about the irrefutable pattern and it's "exploded" complex type. I care only about bound combinations.

pyright works like a charm...

@Hnasar
Copy link
Contributor

Hnasar commented May 25, 2024

Maybe mypy has gotten a little better in recent versions with matching tuples? I found a workaround:

  • assign the tuple being used as the match subject to a named local variable
  • use assert_type(never, tuple[Never, Never]) rather than assert_never(never)

https://mypy-play.net/?mypy=latest&python=3.12&gist=22b719a18f14f6937dc75c484faa0ce3

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug mypy got something wrong topic-match-statement Python 3.10's match statement topic-type-narrowing Conditional type narrowing / binder
Projects
None yet
Development

No branches or pull requests

6 participants