mypy icon indicating copy to clipboard operation
mypy copied to clipboard

False positive `unreachable` when variable is set and reset inside loop

Open adebrecht661 opened this issue 2 years ago • 6 comments

Bug Report

mypy warns about the right side of a condition being unreachable when a variable is reset in an outer loop, then set again inside the inner loop.

To Reproduce

from typing import Optional

test: Optional[float] = None
reveal_type(test)  # Revealed type: Union[builtins.float, None]

for i in range(10):
    test = None
    reveal_type(test)  # Revealed type: None (???)
    for k in range(10):
        if test is None or k > test:
           test = k

Expected Behavior

No error.

Actual Behavior

mypy outputs error: Right operand of "or" is never evaluated. This is clearly because the type of test is re-evaluated at some point, even though it was declared with a type and no statement narrows that type.

This appears related to #7204, #8721, #8865, #13973, #14120, except it can't be solved by declaring the type of the variable before the loop in which it's updated (because that violates no-redef).

Note: I'm not sure why anyone would want to actually do this in practice. But I thought it might provide a useful test case.

Your Environment

  • Mypy version used: 1.1.1
  • Mypy command-line flags: --disable-error-code name-defined, plus a few others that I don't believe could possibly have any relevance.
  • Mypy configuration options from mypy.ini (and other config files): None
  • Python version used: 3.10.10

adebrecht661 avatar Mar 31 '23 13:03 adebrecht661

Similar problem here while using mypy==1.3.0.

My example code:

data = """
{foo}
bar >
baz
"""

prompt = None
reponse = None
lines = data.splitlines()
for line in lines:
    if prompt and reponse:  # mypy error: Right operand of "and" is never evaluated  [unreachable]
        print("Break reached!")  # mypy error: Statement is unreachable  [unreachable]
        break
    if not prompt and line.endswith(">"):
        prompt = line
        continue
    if not reponse and line.endswith("}"):
        reponse = line
        continue

Run output:

$ python example.py
Break reached!
$

Melebius avatar Jun 15 '23 10:06 Melebius

Here looking at this issue after my PR fails on CI. image

patrickelectric avatar Aug 04 '23 18:08 patrickelectric

+1

Here is a smaller example:

state = None
for x in 'ab':
    if state is not None:
        print('reachable')
    state = 'x'

The script output:

reachable

Mypy error:

main.py:4:9: error: Statement is unreachable  [unreachable]
            print('reachable')
            ^~~~~~~~~~~~~~~~~~
Found 1 error in 1 file (checked 1 source file)

✕ mypy failed.

grihabor avatar Jun 09 '24 23:06 grihabor

@grihabor and @Melebius your issues were fixed by https://github.com/python/mypy/pull/18433, though the initial example was not.

A5rocks avatar Jan 22 '25 23:01 A5rocks

Nice, thanks for the ping

grihabor avatar Jan 23 '25 10:01 grihabor

I tend to think the problem underlying the initial example has nothing to do with loops but with promotions, because both float and int are involved.

So, maybe this is a simpler repro:

x: float | None
y: int | None
z: int
reveal_type(x)  # float | None
x = y
reveal_type(x)  # None   !!!!!!!!!
x = z
reveal_type(x)  # int

With "normal" subtypes, the same type of type narrowing works:

class A: ...
class B(A): ...

a: A | None
b: B | None
c: B
reveal_type(a)  # A | None
a = b
reveal_type(a)  # B | None
a = c
reveal_type(a)  # B

Likely, an attempt to solve this could start here:

https://github.com/python/mypy/blob/5a0fa556be741270e25c95a923ef8450d28dd448/mypy/meet.py#L130C1-L145C10

tyralla avatar Jun 03 '25 21:06 tyralla