False positive unreachable with redefined variable in loop
When checking the following code:
from typing import AbstractSet, Iterable, Optional, Set, TypeVar
T = TypeVar('T')
def intersection(seq: Iterable[AbstractSet[T]]) -> Optional[Set[T]]:
ret = None
for elem in seq:
if ret is None:
ret = set(elem)
else:
ret &= elem
return ret
mypy reports:
$ mypy --warn-unreachable intersection.py
intersection.py:11: error: Statement is unreachable
This statement is the ret &= elem inside the else block.
It seems that mypy is using the initial type of ret to conclude that ret is None is always true, ignoring the fact that inside the loop ret is redefined to a different type.
If I place a reveal_type just before the return, mypy does report Union[builtins.set[T`-1], None] as the type, so it figures out the redefinition eventually.
I'm using mypy 0.770 on Pyton 3.8.2, with the --warn-unreachable flag.
Does ret: Optional[Set] = None clear the error?
Yes, which is further evidence that the problem is in the order in which mypy does things: it seems to flag the code as unreachable before the final type for ret has been deduced.
I stumbled over this and created a minimal example:
maximum = None
for x in [2, 1]:
if maximum is None:
maximum = x
else:
maximum = max(x, "maximum")
The last line contains an error. The variable maximum should be passed, but instead, the string "maximum" is passed. Mypy does not detect this because it assumes the last line is unreachable.
Tested with Mypy 0.812 on Python 3.6.9.
I've got a slightly more minimal test case for what I think is the same problem:
def check_sorted(items: list[int]) -> None:
prev = None
for item in items:
reveal_type(prev)
assert prev is None or prev <= item
prev = item
reveal_type(prev)
Output of mypy 1.8.0:
testcase.py:4: note: Revealed type is "None"
testcase.py:5: error: Right operand of "or" is never evaluated [unreachable]
assert prev is None or prev <= item
^~~~~~~~~~~~
testcase.py:7: note: Revealed type is "Union[builtins.int, None]"
Expected output:
testcase.py:4: note: Revealed type is "Union[builtins.int, None]"
testcase.py:7: note: Revealed type is "Union[builtins.int, None]"
It seems mypy is able to correctly infer the type of prev after the loop (line 7), but doesn't feed this back into the start of the loop body (line 4).