Skip to content

Overriding __eq__ can break Optional/Literal narrowing assumptions #15764

@intgr

Description

@intgr

Bug Report

While fixing #15759 (#15760), another unsoundness case occurred to me. By overriding __eq__ in surprising ways, it's not hard to produce unsound results from mypy. Playground link

# Optional narrowing testcase
from typing import Optional, Any, reveal_type

class A:
    def __eq__(self, other: Any) -> bool:
        return other is None

val: Optional[A] = None

if val == A():
    reveal_type(val)  # ❌ N: Revealed type is "__main__.A"
    # ✅ Runtime type is 'NoneType'

It would be possible to check for the existence of __eq__ method. But if any subclasses override __eq__, it's still possible to override __eq__ in a subclass and break it.

Literal+Union narrowing seems to already perform such an __eq__ check, but we can still defeat it by subclassing. Playground link

# Literal narrowing testcase
from typing import Any, reveal_type, Literal

class A:
    pass

class B(A):
    def __eq__(self, other: Any) -> bool:
        return other == 'blab'


val: A | Literal['blab'] = B()

if val == 'blab':
    reveal_type(val)  # ❌ N: Revealed type is "Literal['blab']"
    # ✅ Runtime type is 'B'

Possible Solutions

I couldn't find documentation or prior discussion about this. But I'm guessing these don't come as a surprise to mypy developers?

Possibly the right approach for these kinds of errors could be to simply document the assumptions that mypy relies for its type narrowing? That could also serve as a guide for future mypy developers to explain, what kinds of assumptions we should or shouldn't rely on.

Your Environment

  • Mypy version used: 1.4.1

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugmypy got something wrong

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions