Recently, I've been exploring the type hints functionality in Python. The other day, I ran across what I think is a bug (already known) in mypy.
Here is a minimal working example of the issue I came across:
from fractions import Fraction
my_value = 1 + sum([Fraction(k + 1, 5 ** k) for k in range(5)])
print(f"The result is {my_value}.")
This program runs as you would expect.
python example.py
The result is 64/25.
However, the type-check fails!
mypy example.py
example.py:3: error: List comprehension has incompatible type List[Fraction]; expected List[int] Found 1 error in 1 file (checked 1 source file)
At this point, I was really confused, but after searching for the error, I came across an
issue on GitHub that reported this. In the
discussion, someone explained that the problem is that mypy isn't taking into account the
__radd__
method.
(In general, x + y
is shorthand for x.__add__(y)
. However, if x.__add__
doesn't know
how to deal with y
, then Python tries to use y.__radd__(x)
instead.)
Following the example in the discussion there, I modified the program as follows:
my_value = Fraction(1) + sum([Fraction(k + 1, 5 ** k) for k in range(5)])
The modified version type-checked okay.
At this point, I decided to come up with what my doctoral advisor would call a "Toy Example":
from __future__ import annotations
# Allow self-referential type hints
from typing import Union
class A:
def __init__(self: A, value: int) -> None:
self.value = value
def __repr__(self: A) -> str:
return f"{self.__class__.__name__}({self.value!r})"
def __add__(self: A, other: Union[A, int]) -> A:
print(f"Calling {self!r}.__add__({other!r})")
if isinstance(other, A):
return self.__class__(self.value + other.value)
elif isinstance(other, int):
return self.__class__(self.value + other)
else:
return NotImplemented
def __radd__(self: A, other: int) -> A:
print(f"Calling {self!r}.__radd__({other!r})")
if isinstance(other, int):
return self.__class__(self.value + other)
else:
return NotImplemented
if __name__ == "__main__":
values_to_sum = [A(k) for k in range(1, 5)]
print(f"Computation #1: {sum(values_to_sum)=}")
print(f"Computation #2: {A(5) + sum(values_to_sum)=}")
print(f"Computation #3: {5 + sum(values_to_sum)=}")
print(f"Computation #4: {sum(values_to_sum) + A(5)=}")
print(f"Computation #5: {sum(values_to_sum) + 5=}")
print(f"Computation #6: {A(5) + A(6)=}")
print(f"Computation #7 {5 + A(6)=}")
print(f"Computation #8 {A(5) + 6=}")
Then it executes just fine, but mypy
is upset about line 33, in which
A(10).__radd__(5)
is called. (In the output below, the Calling ...
text is output
before the result of the computation.)
python3 example.py
Calling A(1).__radd__(0) Calling A(1).__add__(A(2)) Calling A(3).__add__(A(3)) Calling A(6).__add__(A(4)) Computation #1: sum(values_to_sum)=A(10) Calling A(1).__radd__(0) Calling A(1).__add__(A(2)) Calling A(3).__add__(A(3)) Calling A(6).__add__(A(4)) Calling A(5).__add__(A(10)) Computation #2: A(5) + sum(values_to_sum)=A(15) Calling A(1).__radd__(0) Calling A(1).__add__(A(2)) Calling A(3).__add__(A(3)) Calling A(6).__add__(A(4)) Calling A(10).__radd__(5) Computation #3: 5 + sum(values_to_sum)=A(15) Calling A(1).__radd__(0) Calling A(1).__add__(A(2)) Calling A(3).__add__(A(3)) Calling A(6).__add__(A(4)) Calling A(10).__add__(A(5)) Computation #4: sum(values_to_sum) + A(5)=A(15) Calling A(1).__radd__(0) Calling A(1).__add__(A(2)) Calling A(3).__add__(A(3)) Calling A(6).__add__(A(4)) Calling A(10).__add__(5) Computation #5: sum(values_to_sum) + 5=A(15) Calling A(5).__add__(A(6)) Computation #6: A(5) + A(6)=A(11) Calling A(6).__radd__(5) Computation #7 5 + A(6)=A(11) Calling A(5).__add__(6) Computation #8 A(5) + 6=A(11)
mypy example.py
example.py:33: error: Argument 1 to "sum" has incompatible type "List[A]"; expected "Iterable[int]" Found 1 error in 1 file (checked 1 source file)
If you remove line 33 in the script (the one that evaluates 5 + sum(values_to_sum)
),
then mypy has no problem!
mypy example.py
Success: no issues found in 1 source file
Note that line 33 isn't the only place that __radd__
ends up being called. It's called
every time sum(values_to_sum)
is evaluated (since you start each sum with a value of the
integer 0). It's also called in line 37, in the computation of 5 + A(6)
. These other
invocations of __radd__
do not mess up mypy. It's only messed up when you __radd__
the
result of a sum
.