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.
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
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)
}
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
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'
mypy knows about this and does not allow such operations:
ws.py:15: error: Unsupported operand types for + ("Decimal" and "float")
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))
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"
A separate funny moment is that in python bool is inherited from int.
>>> int.__subclasses__()
[bool, ...
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
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
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)
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"
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)