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
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