Skip to content

Improve type narrowing for walrus operator in conditional statements #9288

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

Closed
wants to merge 0 commits into from
Closed

Improve type narrowing for walrus operator in conditional statements #9288

wants to merge 0 commits into from

Conversation

kprzybyla
Copy link
Contributor

This improves type narrowing for walrus operator (AssignmentExpr) in conditional statements. Information about the both assignment and condition will be now passed correctly in the if and else map to the ConditionalTypeBinder for type narrowing. Also, literal checks are now correctly interpreted for AssignmentExpr.

Partially fixes #9220 and #9229

@JukkaL
Copy link
Collaborator

JukkaL commented Aug 11, 2020

Thanks for the PR! This addresses some important issues.

I can review this later this week -- but if somebody else wants to review this, please go ahead!

JukkaL pushed a commit that referenced this pull request Aug 14, 2020
…g in if statements (#9297)

This adds support for union narrowing in if statements when condition value has 
defined literal annotations in `__bool__` method. Value is narrowed based on the 
`__bool__` method return annotation and this works even if multiple instances 
defines the same literal value for `__bool__` method return type.

This PR also works well with #9288 and 
makes below example to work as expected:

```python
class A:
    def __bool__(self) -> Literal[True]: ...

class B:
    def __bool__(self) -> Literal[False]: ...

def get_thing() -> Union[A, B]: ...

if x := get_thing():
    reveal_type(x)  # Revealed type is '__main__.A'
else:
    reveal_type(x)  # Revealed type is '__main__.B'
```

Partially fixes #9220
mypy/checker.py Outdated
if else_condition_map is not None:
else_map.update(else_condition_map)

return if_map, else_map
Copy link
Collaborator

Choose a reason for hiding this comment

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

None values are special, but now we always return a non-None value. Should we sometimes still return a None value? The comment for TypeMap explains the distinction.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, we should return None value in case when condition_map is None. Basically there are three possibilities here:

  • assignment_map is None and condition_map is None
def always_false() -> Literal[False]: ...

if x := always_false():
    reveal_type(x)  # E: Statement is unreachable
  • assignment_map is not None and condition_map is None
if (x := 0):
    reveal_type(x)  # E: Statement is unreachable
  • assignment_map is not None and condition_map is not None
if (is_str := maybe_str is not None):
    reveal_type(is_str)  # N: Revealed type is 'builtins.bool'
    reveal_type(maybe_str)  # N: Revealed type is 'builtins.str'

I couldn't find any use case where assignment_map is None and condition_map is not None. I guess that there isn't actually such scenarios where this condition would be true. I added test cases for all above cases and corrected the return statement here.

Also, I modified slightly the behavior of find_isinstance_check_helper method to have a default mechanism for creating type maps (which was basically cut and pasted from the RefExpr case). This was actually needed to make use cases in testWalrusAssignmentAndConditionScopeForFunction test case work properly.


maybe_str: Optional[str]

if (is_str := maybe_str is not None):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does this PR affect simple cases, such as if x := f()? If yes, it's worth testing these separately.

What about if (x := f()) is not None (note that parentheses are different from this test case)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added tests for both if x := f() and if (x := f()) is not None cases.

Also, I found two odd behaviors:

  • Type narrowing without --strict-optional flags seems odd because it can narrows the Union outside of conditional statement even though it is not known which value the variable actually holds. With --strict-optional the last reveal_type in below example emits Union[builtins.str, None].
from typing import Optional

maybe_str: Optional[str]

if (x := maybe_str is not None):
    reveal_type(maybe_str)  # N: Revealed type is 'builtins.str'
else:
    reveal_type(maybe_str)  # N: Revealed type is 'None'

reveal_type(maybe_str)  # N: Revealed type is 'builtins.str'
  • Below case is not handled properly and does not reflect the runtime behavior. Note that this is actually a different use case than the one I added in testWalrusAssignmentAndConditionScopeForLiteral test case because it does not fall into is_true_literal or is_false_literal checks in find_isinstance_check_helper method.
if (x := 5):
    reveal_type(x)  # N: Revealed type is 'builtins.int'
else:
    reveal_type(x)  # E: Statement is unreachable

reveal_type(x)  # N: Revealed type is 'builtins.int'

Copy link
Member

Choose a reason for hiding this comment

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

if (x := maybe_str is not None):

Note that this is parsed as “x := (maybe_str is not None)“

@JukkaL
Copy link
Collaborator

JukkaL commented Aug 28, 2020

@kprzybyla Do you have time to respond to the feedback above? If you are busy, maybe somebody else could finish this PR.

@kprzybyla
Copy link
Contributor Author

kprzybyla commented Sep 1, 2020

@JukkaL Sorry, I had a really busy week, I should be able to take care of this soon this week or maybe even today.

@kprzybyla
Copy link
Contributor Author

@JukkaL Please tell me if there's anything else I should do or correct. Once you have time of course 😃

Copy link
Collaborator

@hauntsaninja hauntsaninja left a comment

Choose a reason for hiding this comment

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

Thanks, and these new test cases are great!

The AssignmentExpr logic looks good to me, although technically I think if either if_assignment_map or if_condition_map is None, you'll want to return None. It's a little contrived, but e.g.:

x: None 
if x := returns_any():
   x = None 

doesn't get flagged currently by --warn-unreachable. (Maybe a little less contrived thanks to your previous PR, since mypy is now smarter about classes whose __bool__ is a literal :-).

I don't have a good sense of potential consequences of now falling back to what was previously just the RefExpr case (having things under explicit if statements made reasoning about things somewhat easier for me), and you now have a small merge conflict, but otherwise looks good!

@cazador481
Copy link

Any word on this? I can fix the merge conflict and put it up for review if that helps.

@hauntsaninja
Copy link
Collaborator

Not sure why Github didn't like that. I opened a new PR at #11202

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Incomplete or inconsistent Literal and Union behavior on conditional checks
5 participants