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 emits two conflicting results for reveal_type #10739

Open
akaptur opened this issue Jun 29, 2021 · 4 comments
Open

Mypy emits two conflicting results for reveal_type #10739

akaptur opened this issue Jun 29, 2021 · 4 comments
Labels
bug mypy got something wrong topic-reveal-type reveal_type() and reveal_locals() topic-union-types

Comments

@akaptur
Copy link

akaptur commented Jun 29, 2021

Bug Report

My colleague @ahuhn and I encountered a situation where mypy emits two different outputs for one reveal_type call:

from decimal import Decimal
from typing import Union

def f(elem: Union[int, str, Decimal]) -> None:
    if isinstance(elem, int):
        return

    for _ in range(3):
        if isinstance(elem, (Decimal, int)) and elem > 10:
            continue
        reveal_type(elem)

    return None

Running mypy emits two lines for the reveal_type call, one correct and the other incorrect. In the incorrect reveal statement, the type narrowing at the top of the function is lost:

$ mypy example.py
example.py:11: note: Revealed type is "Union[builtins.str, decimal.Decimal]"   # expected
example.py:11: note: Revealed type is "Union[builtins.int, builtins.str, decimal.Decimal]"   # wrong

Expected Behavior
Expected only one piece of output for one reveal_type, and for that output to maintain the type narrowing from the top of the function.

Your Environment

We encountered this on mypy 0.812, but it also repros on latest master (currently 416f57b) using python 3.8.6.

A variation of interest: if you add a line of code after the reveal_type, then the second reveal_type output broadens to include Any:

from decimal import Decimal
from typing import Union

def f(elem: Union[int, str, Decimal]) -> None:
    if isinstance(elem, int):
        return

    for _ in range(3):
        if isinstance(elem, (Decimal, int)) and elem > 10:
            continue
        reveal_type(elem)
        elem += 1   # new compared to the example above

    return None

mypy:

$ mypy example.py
example.py:11: note: Revealed type is "Union[builtins.str, decimal.Decimal]"  # correct
example.py:11: note: Revealed type is "Union[builtins.int, Any, builtins.str, decimal.Decimal]"  # even wider, now including Any
example.py:12: error: Unsupported operand types for + ("str" and "int")   # expected and correct
example.py:12: note: Left operand is of type "Union[str, Decimal]"
example.py:12: note: Left operand is of type "Union[int, Any, str, Decimal]"
Found 1 error in 1 file (checked 1 source file)

(In this example, it's trivial to get around the problem by removing the redundant isinstance(int) check, but in our actual code, it wasn't so straightforward.)

@akaptur akaptur added the bug mypy got something wrong label Jun 29, 2021
@9999years
Copy link

I think I'm experiencing the same bug, and the revealed types have no overlap:

import re
from typing import Union, AnyStr, List

def expect(pattern: Union[re.Pattern[AnyStr], AnyStr, List[AnyStr]]) -> None:
    if isinstance(pattern, str):
        return

    if isinstance(pattern, re.Pattern) and isinstance(pattern.pattern, str):
        return

    reveal_type(pattern)
$ mypy mypy_10739.py
mypy_10739.py:11: note: Revealed type is "builtins.list[builtins.str*]"
mypy_10739.py:11: note: Revealed type is "Union[typing.Pattern[builtins.bytes*], builtins.bytes*, builtins.list[builtins.bytes*]]"

@ahuhn
Copy link

ahuhn commented Jul 12, 2021

I did some initial investigation into where the error is and determined that the initial isinstance check is filtering out str and List as expected, and the second isinstance check is adding str back in (but List is not added back in). I did some logging to confirm that it is related to taking a union of (str, int) and (int, Decimal), not that it's popping up to too high a frame and getting back to the initial typing starting point.

  • x starts out typed as x: Union[int, str, Decimal, List]
  • first isinstance removes str and List -> x: Union[int, Decimal]
  • second isinstance adds back in str -> x: Union[int, str, Decimal]

I wrote a non-passing test for mypy/test-data/unit/check-isinstance.test:

[case testModifyLoopForWithIsInstance]
from typing import List, Union
from decimal import Decimal

def f(x: Union[int, str, Decimal, List]) -> None:

    if isinstance(x, (str, List)):
        return

    x + 1

    for _ in [1]:
        x + 1
        # TODO: add a copy of the test with and without `and bool()`
        if isinstance(x, (str, int)) and bool():
            x + 1
            continue
        x + 1
    else:
        x + 1
    x + 1

    return None

f(1)

When looking at the output, note that x is typed as Union[int, str, Decimal], not Union[int, str, Decimal, List]

Expected:
Actual:
  main:12: error: Unsupported operand types for + ("str" and "int") (diff)
  main:12: note: Left operand is of type "Union[int, str, Decimal]" (diff)
  main:15: error: Unsupported operand types for + ("str" and "int") (diff)
  main:15: note: Left operand is of type "Union[int, str]" (diff)
  main:17: error: Unsupported operand types for + ("str" and "int") (diff)
  main:17: note: Left operand is of type "Union[int, str, Decimal]" (diff)
  main:19: error: Unsupported operand types for + ("str" and "int") (diff)
  main:19: note: Left operand is of type "Union[int, str, Decimal]" (diff)
  main:20: error: Unsupported operand types for + ("str" and "int") (diff)
  main:20: note: Left operand is of type "Union[int, str, Decimal]" (diff)

@ahuhn
Copy link

ahuhn commented Aug 2, 2021

I made a PR to fix it! yay :)

I don't fully understand meet_types vs narrow_declared_type yet, so I'll take another look next time I have downtime to understand before I submit.

@ahuhn
Copy link

ahuhn commented Aug 2, 2021

Also, maybe it should be a breaking change on some things and should be meet_types instead of narrow_declared_type. Or something else entirely.

I should think about other edge cases as well.

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-reveal-type reveal_type() and reveal_locals() topic-union-types
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants