chrisphan.com

mypy sum error

An analog clock reading 5:402021-10-28 / 2021-W43-4T17:40:00-05:00 / 0x617b26c0

Categories: programming

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:

example.py Python
1
2
3
4
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.

Bash
$
python example.py
The result is 64/25.

However, the type-check fails!

Bash
$
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:

example.py Python

3

# [...]
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":

example.py Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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.)

Bash
$
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!

Bash
$
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.