Skip to content

Overriding __eq__ can break Optional/Literal narrowing assumptions #15764

Closed

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

Activity

  1. hauntsaninja commented on Feb 6, 2026

    @hauntsaninja
    Collaborator

    I'm in the process of rewriting mypy's type narrowing logic.

    I originally fixed the first form of unsoundness you mention, but in #20756 we are potentially reintroducing unsoundness specifically for None.

    The potential existence of a subclass with custom __eq__ is a hole that we can't really close (although my experience with this rewrites suggests it might be less completely terrible than I would have thought going in to it, as long as you model things you know the exact type for well, like literals).

  2. added a commit that references this issue on Feb 6, 2026
    06a5f5d
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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