4

I want to express that the first parameter is a "list" of the second parameter, and that the result has the same type as the second parameter.

This mysum (ie. not the standard lib sum) should work equally well with int/float/str/list, and any other type that supports +=.

Naively:

def mysum(lst: list[T], start: T) -> T:
    x = start
    for item in lst:
        x += item
    return x    

which produces:

(dev311) go|c:\srv\tmp> mypy sumtype.py
sumtype.py:26: error: Unsupported left operand type for + ("T")
sumtype.py:35: error: Cannot infer type argument 1 of "mysum"
Found 2 errors in 1 file (checked 1 source file)

Second attempt:

from typing import Iterable, Protocol, TypeVar
T = TypeVar('T')

class Addable(Protocol[T]):
    def __add__(self, other: T) -> T:
        ...

class RAddable(Protocol[T]):
    def __radd__(self, other: T) -> T:
        ...

def mysum(lst: Iterable[Addable|RAddable], start: Addable) -> Addable:
    x = start
    for item in lst:
        x += item
    return x

however, this doesn't place any restrictions on the "list" items and start being the same type, so this typechecks:

class Foo:
    def __radd__(self, other: int) -> int:
        return other + 42


mysum([Foo()], [])  # <=== should ideally fail since Foo() and [] are not the same type

and fails with a type-error at runtime:

(dev311) go|c:\srv\tmp> python sumtype.py
Traceback (most recent call last):
  File "c:\srv\tmp\sumtype.py", line 27, in <module>
    mysum([Foo()], [])
  File "c:\srv\tmp\sumtype.py", line 18, in mysum
    x += item
  File "c:\srv\tmp\sumtype.py", line 24, in __radd__
    return other + 42
           ~~~~~~^~~~
TypeError: can only concatenate list (not "int") to list

I'm assuming it can't be this difficult to type-annotate a generic function, so what am I missing..?

thebjorn
  • 26,297
  • 11
  • 96
  • 138

2 Answers2

7

You can make a generic type bound to a protocol that supports __add__ instead:

Type variables can be bound to concrete types, abstract types (ABCs or protocols), and even unions of types

from typing import TypeVar, Protocol

T = TypeVar('T', bound='Addable')

class Addable(Protocol):
    def __add__(self: T, other: T) -> T:
        ...

def mysum(lst: list[T], start: T) -> T:
    x = start
    for item in lst:
        x += item
    return x

Demo: https://mypy-play.net/?gist=ecf4031f21fb81e75d6e6f75d16f3911

blhsing
  • 91,368
  • 6
  • 71
  • 106
  • Your demo throws an error on line 16. – Shmack Sep 02 '22 at 18:43
  • That's the whole point of the demo. It passes the static type check on line 15 and properly fails the type check on line 16, where argument 1 has a mismatching type with argument 2. – blhsing Sep 02 '22 at 18:55
0

I think you forget to pass type argument to (R)Addable in your second attempt.

from typing import Iterable, Protocol, TypeVar
T = TypeVar('T')

class Addable(Protocol[T]):
    def __add__(self: T, other: T) -> T:
        ...

class RAddable(Protocol[T]):
    def __radd__(self: T, other: T) -> T:
        ...

def mysum(lst: Iterable[Addable[T]|RAddable[T]], start: Addable[T]) -> T:
    x = start
    for item in lst:
        x += item
    return x

But this won't work either. Using bounded TypeVar suggested by @blhsing is more elegant and intuitive than this.

The only working example in mypy I can get without bound TypeVar:

from typing import Iterable, Protocol, TypeVar
T = TypeVar('T')

class Addable(Protocol[T]):
    def __add__(self: T, other: T) -> T: ...

def mysum(lst: Iterable[Addable[T]], start: T) -> T:
    x = start
    for item in lst:
        x = item + x
    return x

print(mysum(['a'], 'b'))

str is not RAddable[T] in mypy, int is ok:

from typing import Iterable, Protocol, TypeVar
T = TypeVar('T')

class RAddable(Protocol[T]):
    def __radd__(self: T, other: T) -> T: ...

def mysum(lst: Iterable[RAddable[T]], start: T) -> T:
    x = start
    for item in lst:
        x += item
    return x

print(mysum([1], 2))
YouJiacheng
  • 449
  • 3
  • 11