mypy
mypy copied to clipboard
False positive `comparison-overlap`
Bug Report
If a class variable is changed in a function after a check, mypy assumes that all variables are still the same. This leads to a comparison-overlap error, where none is.
To Reproduce
from enum import Enum
class Types(Enum):
A: str = "A"
B: str = "B"
class A:
def __init__(self) -> None:
self.type: Types = Types.A
def change_type(self) -> None:
self.type = Types.B
def test(self) -> None:
if self.type == Types.A:
self.change_type()
if self.type == Types.B:
print("Should be the case")
Expected Behavior
This is perfectly fine python, and if run in the interpreter, Should be the case
is printed.
Actual Behavior
main.py:17:16: error: Non-overlapping equality check (left operand type: "Literal[Types.A]", right operand type: "Literal[Types.B]") [comparison-overlap]
main.py:18:17: error: Statement is unreachable [unreachable]
Your Environment
- Mypy version used: 1.4.0 (bug is not present in 1.3.0)
- Mypy command-line flags:
--strict
- Python version used: 3.10
The problem is that mypy assumes that narrowed instance variables don't change their type even if (potentially mutating) methods are called:
class A:
def __init__(self) -> None:
self.x: int | str = "foo"
def mutate(self) -> None:
self.x = 0
def get(self) -> str:
if isinstance(self.x, str):
self.mutate()
return self.x # x is actually int now
return "bar"
https://mypy-play.net/?mypy=latest&python=3.11&flags=strict&gist=bd9658f17d55635c932c9cf5d205af36
The pyre type checker is stricter than mypy here and widens the type after any potentially mutating method, but mypy tries to be more convenient and doesn't do that.
The solution is I guess to avoid using mutating methods in type-narrowing branches.
I ran into this exact same issue on Mypy 1.4.1 recently. This error is very common in Pytest unit tests, in which an earlier assertion will raise a type-error after the object is acted upon. I think this behavior is worth fixing.
from enum import Enum
class Fruit(Enum):
APPLE = "apple"
BANANA = "banana"
class Bag:
item: Fruit
def change_fruit(bag: Bag) -> None:
bag.item = Fruit.BANANA
def test_fruit() -> None:
bag = Bag()
assert bag.item == Fruit.APPLE
change_fruit(bag)
# next line gives error: Non-overlapping equality check (left operand type: "Literal[Fruit.APPLE]",
# right operand type: "Literal[Fruit.BANANA]") [comparison-overlap]
assert bag.item == Fruit.BANANA
Yeah, this is a known issue, and an unfortunate side effect of fixing another issue. See 'Narrowing Enum Values Using “==”' in https://mypy-lang.blogspot.com/2023/06/mypy-140-released.html.
Currently I don't know how to fix the false positive in a clean/principled way without regressing some other use cases. The blog post suggests a workaround which allows you to use mutating methods with type narrowing.
Note that that this is not conceptually a new issue, but it previously didn't apply to enums.
Another option to hot-fix it is to use cast
:
def test_fruit() -> None:
bag = Bag()
assert bag.item == Fruit.APPLE
change_fruit(bag)
assert cast(Fruit, bag.item) == Fruit.BANANA