Search code examples
pythonpython-typingmypy

Incompatible `__iadd__` and `__add__` in mypy


I'm writing some code for vectors and matrices where I want to type-check dimensions. I ran into a problem with type-checking __add__ and __iadd__, though. With the simplified example below, mypy tells me that Signatures of "__iadd__" and "__add__" are incompatible. They have exactly the same signatures, though, so what am I doing wrong?


from __future__ import annotations

from typing import (
    Generic,
    Literal as L,
    TypeVar, 
    overload,
    assert_type
)


_D1 = TypeVar("_D1")
_D2 = TypeVar("_D2")
_D3 = TypeVar("_D3")

# TypeVarTuple is an experimental feature; this is a work-aroudn
class Shape:
    """Class that works as a tag to indicate that we are specifying a shape."""
class Shape1D(Shape, Generic[_D1]): pass
class Shape2D(Shape, Generic[_D1,_D2]): pass


_Shape = TypeVar("_Shape", bound=Shape)
Scalar = int | float


class Array(Generic[_Shape]):
    @overload # Adding witht the same shape
    def __add__(self: Array[_Shape], other: Array[_Shape]) -> Array[_Shape]:
        return Any # type: ignore
    @overload # Adding with a scalar
    def __add__(self: Array[_Shape], other: Scalar) -> Array[_Shape]:
        return Any # type: ignore
    def __add__(self, other) -> Array:
        return self # Dummy implementation
        
    @overload # Adding witht the same shape
    def __iadd__(self: Array[_Shape], other: Array[_Shape]) -> Array[_Shape]:
        return Any # type: ignore
    @overload # Adding with a scalar
    def __iadd__(self: Array[_Shape], other: Scalar) -> Array[_Shape]:
        return Any # type: ignore
    def __iadd__(self, other) -> Array:
        return self # Dummy implementation
    
    # Adding with a scalar
    def __radd__(self: Array[_Shape], other: Scalar) -> Array[_Shape]:
        return Any # type: ignore


    
A = Array[Shape2D[L[3],L[4]]]()

reveal_type(A + 1.0) ; assert_type(A + 1.0, Array[Shape2D[L[3],L[4]]])
reveal_type(1.0 + A) ; assert_type(1.0 + A, Array[Shape2D[L[3],L[4]]])
reveal_type(A + A)   ; assert_type(A + A,   Array[Shape2D[L[3],L[4]]])

A += 1.0
A += A

Get the code in a playground here.


Solution

  • It's a bug they're looking at correcting, see mypy issue #6225 (and the older, related, issue #4985). They're trying to protect against some problems that can occur when related operators are overloaded piecemeal on a parent and child class, but oops, it doesn't handle the overloads on a single class correctly.

    There hasn't been any activity on it in a few years though.

    That said, I'm not seeing a need for @overload here. Your return type is always the same, not dependent on argument types, so you could just skip the @overload variations and use a single coherent type annotation per overload:

    class Array(Generic[_Shape]):
        def __add__(self: Array[_Shape], other: Array[_Shape] | Scalar) -> Array[_Shape]:
            if not isinstance(other, (Array, int, float)):
                return NotImplemented
            return self
            
        def __iadd__(self: Array[_Shape], other: Array[_Shape] | Scalar) -> Array[_Shape]:
            if not isinstance(other, (Array, int, float)):
                return NotImplemented
            return self
        
        # Adding with a scalar
        def __radd__(self: Array[_Shape], other: Array[_Shape] | Scalar) -> Array[_Shape]:
            if not isinstance(other, (Array, int, float)):
                return NotImplemented
            return self