Skip to content

Non-overlapping identity check when member variable mutated by function call #9005

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

Open
Hawk777 opened this issue Jun 16, 2020 · 5 comments
Open
Labels

Comments

@Hawk777
Copy link

Hawk777 commented Jun 16, 2020

I get the following error in a situation where I think I shouldn’t:

test.py:25: error: Non-overlapping identity check (left operand type: "Literal[MyEnum.FOO]", right operand type: "Literal[MyEnum.BAR]")

Given the following code:

import enum


@enum.unique
class MyEnum(enum.Enum):
    FOO = enum.auto()
    BAR = enum.auto()


class MyClass:
    __slots__ = (
        "_my_value",
    )

    _my_value: MyEnum

    def __init__(self) -> None:
        super().__init__()
        self._my_value = MyEnum.FOO

    def do_thing(self) -> None:
        if self._my_value is MyEnum.FOO:
            self._set_my_value_to_bar()
            # self._my_value = MyEnum.BAR
            if self._my_value is MyEnum.BAR:
                print("And yet, the value was BAR!")

    def _set_my_value_to_bar(self) -> None:
        self._my_value = MyEnum.BAR


if __name__ == "__main__":
    x = MyClass()
    x.do_thing()

If I uncomment the commented line, the error goes away. It seems that Mypy does not understand that a method call might mutate member variables of the class, obsoleting any prior checks of their values.

This is when running version 0.780 with --strict.

@JukkaL
Copy link
Collaborator

JukkaL commented Jun 19, 2020

Yeah, this behavior is confusing. #9021 fixed a similar issue with booleans. A possible fix would be to omit non-overlapping equality checks based on enum values, since they are likely to result in false positives.

@Hawk777
Copy link
Author

Hawk777 commented Jun 19, 2020

What does this have to do with enums, specifically? It seems to me that values of any type could run into this; the real problem is that making a method call ought to invalidate prior knowledge of the value.

@JelleZijlstra
Copy link
Member

@Hawk777 it's a tradeoff, but my sense is that doing so would invalidate the precise type too often, which would then lead to annoying false positives.

@JukkaL
Copy link
Collaborator

JukkaL commented Jun 19, 2020

Yeah, this is all based on heuristics. There's no way we can reliably decide whether a function or method (or operator) could modify some inferred type as a side effect, so we make the simplification of assuming that it doesn't happen. This works well in practice, except for certain idioms which tend to generate false positives.

@intgr
Copy link
Contributor

intgr commented Oct 17, 2022

One work-around is using no-op assignments whenever there are side-effects, to make mypy forget its learned constraints:

assert foo.status == 'spam'
foo.refresh_from_db()
foo = foo  # <-- Work-around for https://github.com/python/mypy/issues/9005
assert foo.status == 'eggs'
#          ^^^^^^ mypy no longer complains

There's no way we can reliably decide whether a function or method (or operator) could modify some inferred type as a side effect, so we make the simplification of assuming that it doesn't happen.

Maybe creating an annotation with PEP 593 Annotated[] to explicitly mark arguments that are known to be mutated?

E.g.

class Widget:
    def refresh_from_db(self: Annotated[Widget, mypy_extensions.Mutates]) -> None:
        self.status = ...

Then every time x.refresh_from_db() is called, mypy forgets any constraints it had learned for x.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants