I'm trying to get to grips with mypy. As an exercise, I'm trying to figure out the right type annotations for some common higher-order functions. But I don't quite understand why the following code doesn't type check.
test.py
from typing import Iterable, TypeVar, Callable
T1 = TypeVar('T1')
T2 = TypeVar('T2')
def chain(
functions: Iterable[Callable[[T1], T1]]
) -> Callable[[T1], T1]:
def compose(
f: Callable[[T1], T1],
g: Callable[[T1], T1]
) -> Callable[[T1], T1]:
def h(x: T1) -> T1:
return g(f(x))
return h
def identity(x: T1) -> T1:
return x
return reduce(functions, compose, identity)
def reduce(
items: Iterable[T1],
op: Callable[[T2, T1], T2],
init: T2
) -> T2:
for item in items:
init = op(init, item)
return init
def add_one(x):
return x + 1
def mul_two(x):
return x * 2
assert chain([add_one, mul_two, mul_two, add_one])(7) == 33
The code runs correctly in python, but mypy test.py produces the following error message (I've formatted it slightly for readability):
test.py:21: error: Argument 2 to "reduce" has incompatible type
"Callable[
[Arg(Callable[[T1], T1], 'f'), Arg(Callable[[T1], T1], 'g')],
Callable[[T1], T1]
]";
expected
"Callable[
[Callable[[T1], T1], Callable[[T1], T1]],
Callable[[T1], T1]
]"
I'm not sure where Arg is coming from. I couldn't find anything about it in the documentation for typing, and the only reference to it in the mypy documentation says that it is a deprecated feature.
My only thought is that it might have something to do with the fact that compose produces a closure.
This is mypy version 0.720 and Python version 3.7.3.
So it seems as if the error message might be misleading. After adding the following code, the error disappears:
from typing import overload
@overload
def reduce(
items: Iterable[T1],
op: Callable[[T1, T1], T1],
init: T1
) -> T1: ...
@overload
def reduce(
items: Iterable[T1],
op: Callable[[T2, T1], T2],
init: T2
) -> T2: ...
But it's still not clear to me what is going on here.
This is a strange issue, which seems to be a bug in mypy, because pytype and pyre correctly checks your code.
Mypy fails with your code but successfully checks the code if you change your identity function to:
def identity(x: T1, /) -> T1:
return x
The only difference is that the function now only accepts positional arguments.
Note, I'd use functools.reduce instead, so that you can leverage typings from typeshed. In that case you would need to call return reduce(compose, functions, identity).
BONUS:
There's also a strange pyre issue, if you move the identity function outside compose, then I get an error
Mutually recursive type variables [36]: Solving type variables for call `reduce` led to infinite recursion.
But get fixed if you replace your reduce with functools.reduce.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With