Skip to content

Inconsistent erroring for wider argument type in operator override when using overloads (false negative) #22304

Open

Description

@aatle

Bug Report

When overriding an overridden binary operator (e.g. __mul__, __add__) inside a subclass, mypy enforces that the subclass's operator argument is not a wider type than the superclass's because of operator's "double dispatch" behavior. This is desired behavior I think.

However, mypy does not catch some cases that are equivalent to cases that it errors on, specifically non-overloads, and so is inconsistent.

To Reproduce

from typing import Self, overload


class Base:
    def __mul__(self, other: float) -> Self:
        return self
    
    
class One(Base):
    def __mul__(self, other: str | float) -> Self:
        return self


class Two(Base):
    @overload
    def __mul__(self, other: float) -> Self: ...
    @overload
    def __mul__(self, other: str) -> Self: ...
    def __mul__(self, other: str | float) -> Self:
        return self


class Three(Base):
    def __mul__(self, other: object) -> Self:
        return self

https://mypy-play.net/?mypy=latest&python=3.14&gist=b3f9bba5d1bf5dc98f3f06d123e97932

Expected Behavior

Expected: One and Two and Three all have their __mul__ method marked as an error, and are consistent.

(Alternatively, maybe all pass and have no errors.)

Actual Behavior

Only Two has its __mul__ method marked as an error, even though both One and Two effectively have the same method signature, and Three is also broader.

main.py:15: error: Signature of "__mul__" incompatible with supertype "Base"  [override]
main.py:15: note:      Superclass:
main.py:15: note:          def __mul__(self, float, /) -> Two
main.py:15: note:      Subclass:
main.py:15: note:          @overload
main.py:15: note:          def __mul__(self, float, /) -> Two
main.py:15: note:          @overload
main.py:15: note:          def __mul__(self, str, /) -> Two
main.py:15: note: Overloaded operator methods can't have wider argument types in overrides
Found 1 error in 1 file (checked 1 source file)

Your Environment

  • Mypy version used: 2.4.0
  • Python version used: 3.14

Activity

  1. changed the title [-]Inconsistent erroring for wider argument type in operator override when using overloads[/-] [+]Inconsistent erroring for wider argument type in operator override when using overloads (false negative)[/+] on Oct 26, 2026
  2. inchang-ing commented on Oct 28, 2026

    @inchang-ing
    Contributor

    Thanks for the clear repro — confirmed on mypy 1.18.2, same output as your 2.4.0 run: only Two errors, One and Three are silent.

    Root cause — in mypy/checker.py::check_override, the "operator methods can't extend the domain" rule is gated on the override being an overload:

    elif isinstance(override, Overloaded) and self.is_forward_op_method(name):   # line 2501

    For One, override is a plain CallableType, so that branch is skipped. The generic LSP check just above it (is_subtype(override, original), line 2499) passes, because widening a parameter is contravariance-correct for an ordinary method — which is exactly why the operator rule exists as a separate constraint (double dispatch: the reverse method on the supertype can be selected for operands the subclass widened into). So the operator-specific rule is currently only enforced on the Overloaded path.

    The helper it uses, get_op_other_domain (line 2454), already handles CallableType as well as Overloaded, so dropping the isinstance(override, Overloaded) condition would put all three classes on the same footing and report One and Three too — the "all error" outcome you listed first.

    Before I write that patch I'd like your call on the direction, because it isn't risk-free: every existing override of a forward operator method with a wider argument type that isn't written as an overload would start erroring. That pattern is reasonably common in numeric/tensor-style code, so it's a user-visible behavior change, and mypy's own self-check would likely surface some of them. The other consistent reading is that the overload-only rule is the intended scope and One/Three should stay silent.

    Happy to implement the first option — plus cases in the override test data — if you confirm that's the direction you want.

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