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

Narrowing and exhaustiveness errors when matching on a tuple of optionals #14731

Open
linabutler opened this issue Feb 18, 2023 · 1 comment · May be fixed by #17109
Open

Narrowing and exhaustiveness errors when matching on a tuple of optionals #14731

linabutler opened this issue Feb 18, 2023 · 1 comment · May be fixed by #17109
Labels
bug mypy got something wrong

Comments

@linabutler
Copy link

linabutler commented Feb 18, 2023

Bug Report

When using match on a tuple of optionals, Mypy doesn't seem to:

  1. Narrow the type from Optional[T] to T in non-optional cases, even if all the None cases have been handled.
  2. Recognize that the match is exhaustive.

This feels similar to #12267 (comment), or maybe #12364 or #13989, but the example is a little different; apologies if it's a dupe / already known—and thanks for taking a look!

To Reproduce

https://mypy-play.net/?mypy=latest&python=3.11&gist=b6c10a27006d7b5ad55729467c2c5de8

Expected Behavior

No errors. 😊

I've used this match-on-a-tuple pattern in Rust and Swift a few times, which is why I was a little surprised that Mypy didn't like it.

Actual Behavior

main.py:4: error: Missing return statement  [return]
main.py:9: error: Item "None" of "Optional[str]" has no attribute "encode"  [union-attr]

Your Environment

  • Mypy version used: 1.0.0
  • Python version used: 3.11
@bluenote10
Copy link
Contributor

For some reason the link to mypy playground is currently giving me an error Failed to fetch the gist: AxiosError: Request failed with status code 403.

Is this issue e.g. about the following?

def foo(a: str | None, b: str | None) -> None:
    match a, b:
        case None, None:
            return
        case a_matched, None:
            reveal_type(a_matched)
            return
        case None, b_matched:
            reveal_type(b_matched)
            return        
        case a_matched, b_matched:
            reveal_type(a_matched)
            reveal_type(b_matched)
            return   

The revealed types are:

main.py:10: note: Revealed type is "Union[builtins.str, None, builtins.str, None]"
main.py:13: note: Revealed type is "Union[builtins.str, None, builtins.str, None]"
main.py:16: note: Revealed type is "Union[builtins.str, None, builtins.str, None]"
main.py:17: note: Revealed type is "Union[builtins.str, None, builtins.str, None]"

and I was wondering if this is supposed to infer non-optional value even without explicit guards.

Using explicit guards seems to work in this case, but it is syntactically more clumsy than an if elif chain that there is almost no practical reason to use match.

def foo(a: str | None, b: str | None) -> None:
    match a, b:
        case None, None:
            return
        case (a_matched, None) if a_matched is not None:
            reveal_type(a_matched)
            return
        case (None, b_matched) if b_matched is not None:
            reveal_type(b_matched)
            return        
        case (a_matched, b_matched) if a_matched is not None and b_matched is not None:
            reveal_type(a_matched)
            reveal_type(b_matched)
            return        

edpaget added a commit to edpaget/mypy that referenced this issue Apr 9, 2024
Allows sequence pattern matching of tuple types with union members to
use an unmatched pattern to narrow the possible type of union member to
the type that was not matched in the sequence pattern.

For example given a type of `tuple[int, int | None]` a pattern match
like:

```
match tuple_type:
    case a, None:
         return
    case t:
         reveal_type(t) # narrows tuple type to tuple[int, int]
         return
```

The case ..., None sequence pattern match can now narrow the type in
further match statements to rule out the None side of the union.

This is implemented by moving the original implementation of tuple
narrowing in sequence pattern matching since its functionality should be
to the special case where a tuple only has length of one. This
implementation does not hold for tuples of length greater than one since
it does not account all combinations of alternative types.

This replace that implementation with a new one that  builds the rest
type by iterating over the potential rest type members preserving
narrowed types if they are available and replacing any uninhabited types
with the original type of tuple member since these matches are only
exhaustive if all members of the tuple are matched.

Fixes python#14731
@edpaget edpaget linked a pull request Apr 9, 2024 that will close this issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug mypy got something wrong
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants