Challenge accepted.
from dataclasses import dataclass
from typing import Protocol, Self, TypeVar
class Semigroup(Protocol):
def __add__(self, other: Self) -> Self:
...
T = TypeVar("T", bound=Semigroup)
def join_stuff(first: T, *rest: T) -> T:
accum = first
for x in rest:
accum += x
return accum
@dataclass
class C:
x: int
@dataclass
class D:
x: int
def __add__(self, other: Self) -> Self:
return type(self)(self.x + other.x)
@dataclass
class E:
x: int
def __add__(self, other: Self) -> Self:
return type(self)(self.x + other.x)
_: type[Semigroup] = D
_ = E
def doit() -> None:
print(join_stuff(1,2,3))
print(join_stuff((1,), tuple(), (2,)))
print(join_stuff("a", "b", "c"))
print(join_stuff(D(1), D(2)))
print(join_stuff(D(1), 3))
print(D(1) + 3) # caught by mypy
print(D(1) + E(3)) # caught by mypy
print(join_stuff(1,2,"a")) # Not caught by mypy
print(join_stuff(C(1), C(2))) # caught by mypy
doit()
Now, this doesn't
quite work to my satisfaction. Mypy lets you
freely mix and match values of incompatible types, and I don't know
how to fix that. Basically, if you directly try to add a D and an
int, mypy will yell at you, but there's no way I've found to insist
that the arguments to join_stuff, in addition to being Semigroups,
are all of the compatible types. It looks like mypy is checking
join_stuff as if Semigroup were a concrete class, so once you're
inside join_stuff, the actual types of the arguments become irrelevant.
However, it will correctly tell you that it can't accept arguments that
don't define addition at all, and that's better than nothing.