mypy icon indicating copy to clipboard operation
mypy copied to clipboard

False positive `comparison-overlap`

Open fabi321 opened this issue 1 year ago • 4 comments

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")

Real world example

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

playground link

  • Mypy version used: 1.4.0 (bug is not present in 1.3.0)
  • Mypy command-line flags: --strict
  • Python version used: 3.10

fabi321 avatar Jun 24 '23 12:06 fabi321

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.

tmke8 avatar Jun 25 '23 13:06 tmke8

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

TylerYep avatar Jul 01 '23 20:07 TylerYep

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.

JukkaL avatar Jul 03 '23 15:07 JukkaL

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

kamilglod avatar Jul 10 '23 06:07 kamilglod