DEV Community

ArtyomKaltovich
ArtyomKaltovich

Posted on • Updated on

The Numeric Tower: Adventures of Numbers in Python and mypy

If you've ever felt like you've been bogged down in meetings and discussions that go on and on and there's still no solution, maybe you will fill better if you know that there is a 5 year old issue in mypy about an integer not being a number.

Image description

Type hints in python are an interesting topic. People from statically typed languages ​​do not understand how it was possible to create a language without them, and then add them. Lovers of dynamics, do not understand why they should spend time adding types if the code already works, and type analyzers only give you false positive results. While python developers continue to read and debug the code, trying to understand what the author meant and adding type annotations, if they managed to understand it.

However, screwing up types and checking them somewhere on the side really has some problems, for example, we can look into such a seemingly simple topic as numbers.

Python has the following built-in types for numbers: integers, floating point and fixed point reals, rational fractions, and even complex numbers. These types implement certain interfaces (ABCs) organized into the numeric tower (Number, Complex, Real, Rational and Integral). And this is where the problems begin. Some solutions seem clear and valid, for example, you can always pass an int to a function that takes a float, and you can always pass both a float and an int to a function that takes a complex.

def sin(a: float):
    print(isinstance(a, float))


def cos(a:complex):
    print(isinstance(a, complex))


sin(1) # prints False
cos(4.5) # also prints False
Enter fullscreen mode Exit fullscreen mode

Checking this code with mypy will output Success: no issues found in 1 source file. And from a mathematical point of view, this makes sense: any real number is complex, and any integer is real. But there are purists who will not accept such code, for example, in rust, you cannot pass integers to functions that expect a real number.

fn f(a: f32) {
}

fn main() {
    f(4)
}
Enter fullscreen mode Exit fullscreen mode
   Compiling playground v0.0.1 (/playground)
error[E0308]: mismatched types
 --> src/main.rs:6:7
  |
6 | f(4)
  | -^
  | | |
  | | expected `f32`, found integer
  | | help: use a float literal: `4.0`
  | arguments to this function are incorrect
Enter fullscreen mode Exit fullscreen mode

As you can see, even in python, a beautiful mathematical abstraction begins to flow, type checks pass in mypy, but isinstance checks return False. So, the type in the signature and the type in isinstance are not the same thing, however, even this is understandable, given the __subclasshook__ and the dynamism of the language.

Also in python, decimal and float are not interchangeable and compatible, although from the mathematical point of view, both are real numbers, but the creators of the language decided that mixing two types in one operation is not worth it, as this can lead to the loss of precision.

>>> Decimal(1) + 2.5
TypeError: unsupported operand type(s) for +: 'decimal.Decimal' and 'float'
Enter fullscreen mode Exit fullscreen mode

mypy knows about this and does not allow such operations:

ws.py:15: error: Unsupported operand types for + ("Decimal" and "float")
Enter fullscreen mode Exit fullscreen mode

Also, a Decimal cannot be passed to a function that expects a complex variable, and an integer cannot be passed to a function that expects a Decimal. Although the mathematical abstraction, in theory, should not stop working from the fact that we changed real floating-point numbers to fixed-point numbers, they still remain real and mathematical operations that are valid on Decimal must also be performed on int. But mypy won't accept such code.

from decimal import Decimal
from numbers import Number


def sin(a: Decimal):
    ...


def cos(a:complex):
    ...


sin(1)
cos(Decimal(1))
Enter fullscreen mode Exit fullscreen mode
ws.py:13: error: Argument 1 to "sin" has incompatible type "int"; expected "Decimal"
ws.py:14: error: Argument 1 to "cos" has incompatible type "Decimal"; expected "complex"
Enter fullscreen mode Exit fullscreen mode

A separate funny moment is that in python bool is inherited from int.

>>> int.__subclasses__()
[bool, ...
Enter fullscreen mode Exit fullscreen mode
from decimal import Decimal


def f(x: float):
    pass

f(Decimal(1)) # ws.py:7: error: Argument 1 to "sin" has incompatible type "Decimal"; expected "float"
f(1==0) # And these lines
f(True) # pass the check
Enter fullscreen mode Exit fullscreen mode

That is, it is impossible to calculate the f function from one, but from the truth - it is possible.

Now back to the issue from the beginning of the article. int, Decimal and float are Number.

isinstance(1, Number) # True
isinstance(2.5, Number) # True
isinstance(Decimal(2.5), Number) # True
Enter fullscreen mode Exit fullscreen mode

But mypy doesn't think so.

from decimal import Decimal
from numbers import Number


def sin(x: Number):
    pass

sin(Decimal(1))
sin(1)
sin(2.5)
sin(True)
Enter fullscreen mode Exit fullscreen mode
ws.py:8: error: Argument 1 to "sin" has incompatible type "Decimal"; expected "number"
ws.py:9: error: Argument 1 to "sin" has incompatible type "int"; expected "number"
ws.py:10: error: Argument 1 to "sin" has incompatible type "float"; expected "number"
ws.py:11: error: Argument 1 to "sin" has incompatible type "bool"; expected "number"
Enter fullscreen mode Exit fullscreen mode

sin(Number(1)) won't work either, since Number is an abstract class.

Python is quite a pleasant language to use, and it is not for nothing that it has become one of the most popular (and by some estimates, the most popular language). And many of the architectural decisions made during the implementation of the language and the interpreter are worthy of study. But, sometimes even such simple entities as numbers lead to the need to make complex and ambiguous decisions, allowing several bugs and non-obviousness in the process. So, if suddenly your boss is dissatisfied with your architecture, you can try to excuse yourself with the fact that not only you fail to create an ideal type hierarchy, even Guido does not always succeed.

Top comments (0)