Skip to content

Inconsistency with unions and overloads in when checking overlapping op methods #2129

Closed
@Michael0x2a

Description

@Michael0x2a

As a part of my adventures in attempting to add semi-usable stubs for the fractions and statistics modules, I ran into an edge case involving the __add__ and __radd__ methods (or any pair of operator methods). Here is a simplified version of decimal.Decimal:

from typing import Union

class FakeDecimal1:
    def __add__(self, other: Union[int, FakeDecimal1]) -> FakeDecimal1:
        return FakeDecimal1()
    def __radd__(self, other: Union[int, FakeDecimal1]) -> FakeDecimal1:
        return FakeDecimal1()

When typechecking this, I got the following (and somewhat cryptic) error message:

experiments\test.py: note: In member "__radd__" of class "FakeDecimal":
experiments\test.py:6: error: Forward operator "__add__" is not callable

It seems mypy wanted me to do this instead:

from typing import Union

class FakeDecimal2:
    def __add__(self, other: Union[int, FakeDecimal2]) -> FakeDecimal2:
        return FakeDecimal2()
    def __radd__(self, other: int) -> FakeDecimal2:
        return FakeDecimal2()

However, the following code which uses overrides instead of Union does not result in any errors, which feels inconsistent:

class FakeDecimal3:
    @overload
    def __add__(self, other: int) -> FakeDecimal3:
        return FakeDecimal2()
    @overload
    def __add__(self, other: FakeDecimal3) -> FakeDecimal3:
        return FakeDecimal3()
    @overload
    def __radd__(self, other: int) -> FakeDecimal3:
        return FakeDecimal2()
    @overload
    def __radd__(self, other: FakeDecimal3) -> FakeDecimal3:
        return FakeDecimal2()

After some digging, it appeared to me that the reason for this behavior is because the check_overlapping_op_methods within checker.py handles the case where forward_type is either a CallableType, Overloaded, or AnyType, but not the case where it's a UnionType.

Is this intentional, or an oversight? I think this is a bug, mainly since if mypy accepts version 3, it ought to accept verison 1? Or perhaps the fact that __radd__ should never need to accept a value of the same type as its instance means that the version 2 is more correct and versions 1 and 3 ought to be rejected?

(For what it's worth, adding a new case to make UnionType do the same thing as the Overloaded case makes version 1 pass.)

This is perhaps related: #1442

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions