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

mypy's narrowing for match statements sometimes fails when the subject of the match is a function call #12998

Closed
zenbot opened this issue Jun 20, 2022 · 4 comments · Fixed by #16503
Labels
bug mypy got something wrong topic-match-statement Python 3.10's match statement topic-type-narrowing Conditional type narrowing / binder

Comments

@zenbot
Copy link

zenbot commented Jun 20, 2022

Bug Report

mypy's narrowing for match statements sometimes fails when the subject of the match is a function call.

To Reproduce

from typing_extensions import assert_never

def fn() -> int | None:
    ...

match fn(): 
    case int():
        pass
    case None:
        pass
    case _ as unreachable:
        assert_never(unreachable)

Expected Behavior

The code above should pass a type check because case statements handle all possible return values of fn().

Actual Behavior

$ mypy --strict bad.py
bad.py:12: error: Argument 1 to "assert_never" has incompatible type "Optional[int]"; expected "NoReturn"
Found 1 error in 1 file (checked 1 source file)

Note

Assigning the return value of fn() to a variable and using that variable as the subject of the match does pass type checking:

from typing_extensions import assert_never

def fn() -> int | None:
    ...

rv = fn()

match rv: 
    case int():
        pass
    case None:
        pass
    case _ as unreachable:
        assert_never(unreachable)

Handling all cases within the one case statement also passes type checking:

from typing_extensions import assert_never

def fn() -> int | None:
    ...

match fn(): 
    case int() | None:
        pass
    case _ as unreachable:
        assert_never(unreachable)

mypy's output for both of the above:

$ mypy --strict bad.py
Success: no issues found in 1 source file

Your Environment

  • Mypy version used: mypy 0.961 (compiled: yes)
  • Mypy command-line flags: --strict
  • Python version used: Python 3.10
  • Operating system and version: macOS Monterey 12.3.1
@zenbot zenbot added the bug mypy got something wrong label Jun 20, 2022
@AlexWaygood AlexWaygood added topic-match-statement Python 3.10's match statement topic-type-narrowing Conditional type narrowing / binder labels Jun 20, 2022
@Corwinpro
Copy link

Corwinpro commented Aug 25, 2023

Potential workaround (yet this is certainly a bug):

match value := fn():
    ... 

Overall, I think the problem is with Callable:

def foo(f: Callable[[], Result[int, str]]) -> str:
    match f():  # does not work, mypy complains
        case Ok(value):
            return f"{value}"
        case Err(e):
            raise RuntimeError(e)

but this is OK:

def foo(f: Callable[[], Result[int, str]]) -> str:
    value = f()
    match value:  # does work, mypy is happy
        case Ok(value):
            return f"{value}"
        case Err(e):
            raise RuntimeError(e)

@bwo
Copy link
Contributor

bwo commented Oct 30, 2023

Something similar seems to happen with tuples. Mypy doesn't like this:

def foo(i: int | str, j : int | str) -> int:
    match (i, j):
        case int(), int(): return i + j
        case int(), str(): return i + len(j)
        # and so on

But this is fine:

def foo(i: int | str, j : int | str) -> int:
    t = (i, j)
    match t:
        case int(), int(): return t[0] + t[1]
        case int(), str(): return t[0] + len(t[1])
    # and so on

however, even in the latter case, you can't put an assert_never(t) at the end.

edpaget added a commit to edpaget/mypy that referenced this issue Nov 16, 2023
Fixes python#12998

mypy can't narrow match statements with functions subjects because the
callexpr node is not a literal node. This adds a 'dummy' literal node
that the match statement visitor can use to do the type narrowing.

The python grammar describes the the match subject as a named expression
so this uses that nameexpr node as it's literal.
edpaget added a commit to edpaget/mypy that referenced this issue Nov 16, 2023
Fixes python#12998

mypy can't narrow match statements with functions subjects because the
callexpr node is not a literal node. This adds a 'dummy' literal node
that the match statement visitor can use to do the type narrowing.

The python grammar describes the the match subject as a named expression
so this uses that nameexpr node as it's literal.
hauntsaninja added a commit that referenced this issue Feb 17, 2024
Fixes #12998

mypy can't narrow match statements with functions subjects because the
callexpr node is not a literal node. This adds a 'dummy' literal node
that the match statement visitor can use to do the type narrowing.

The python grammar describes the the match subject as a named expression
so this uses that nameexpr node as it's literal.

---------

Co-authored-by: hauntsaninja <hauntsaninja@gmail.com>
@hauntsaninja
Copy link
Collaborator

Btw, I'm curious if any of the folks on this thread are encountering this in open source projects. I'd love to add more coverage for match statement to mypy_primer

hamdanal pushed a commit to hamdanal/mypy that referenced this issue Feb 20, 2024
Fixes python#12998

mypy can't narrow match statements with functions subjects because the
callexpr node is not a literal node. This adds a 'dummy' literal node
that the match statement visitor can use to do the type narrowing.

The python grammar describes the the match subject as a named expression
so this uses that nameexpr node as it's literal.

---------

Co-authored-by: hauntsaninja <hauntsaninja@gmail.com>
@tamird
Copy link
Contributor

tamird commented Apr 29, 2024

It seems this is still an issue in the presence of match await.

https://mypy-play.net/?mypy=master&python=3.12&gist=53b6a8349cf88907ed6608f7fdb05050

from typing import assert_never
from collections.abc import Awaitable

async def fn() -> int | None:
    return None

async def foo() -> None:
    match await fn(): 
        case int():
            pass
        case None:
            pass
        case _ as unreachable:
            assert_never(unreachable)
main.py:14: error: Argument 1 to "assert_never" has incompatible type "int | None"; expected "NoReturn"  [arg-type]

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

Successfully merging a pull request may close this issue.

6 participants