Home > Net >  mypy with nyumbers.Number
mypy with nyumbers.Number

Time:07-18

I was hoping to use mypy's static duck typing facility to write a function that can process a sequence of number-like objects, where "number-like" is defined by numbers.Number:

from numbers import Number
from typing import Sequence

def something_numerical(xs: Sequence[Number]) -> Number:
  ...

print(multiply([1., 2., 3.]))

However, when I call this code with a list of floats or ints, I get a mypy error:

$ print(multiply([1., 2., 3.]))
foo/foo.py:9: error: List item 0 has incompatible type "float"; expected "Number"
foo/foo.py:9: error: List item 1 has incompatible type "float"; expected "Number"
foo/foo.py:9: error: List item 2 has incompatible type "float"; expected "Number"

I realize that the float type is not a subclass of numbers.Number. However, the numbers module provides a set of abstract base classes that are intended to be used to check whether an object has the requisite methods to do numerical operations. How might I rewrite this code so that (1) it can still process ints, floats, fractios.Fraction, and so on, and (2) so that it passes type checking by mypy?

CodePudding user response:

It seems using Union will be the way to go:

from numbers import Number
from typing import Sequence, Union
from fractions import Fraction


def something_numerical(xs: Sequence[Union[Number, float]]) -> Union[Number, float]:
    return sum(xs)


if __name__ == '__main__':
    print(something_numerical([1.2, 2, Fraction(1, 2)]))

CodePudding user response:

It was stated in @bzu answer, but I'd like to add some explanation to it.

First thing to note: issubclass(int, Number) and issubclass(float, Number) both evaluate to True. This is very surprising type-checking behavior, but it was standardized in PEP484:

Rather than requiring that users write import numbers and then use numbers.Float etc., this PEP proposes a straightforward shortcut that is almost as effective: when an argument is annotated as having type float, an argument of type int is acceptable; similar, for an argument annotated as having type complex, arguments of type float or int are acceptable. This does not handle classes implementing the corresponding ABCs or the fractions.Fraction class, but we believe those use cases are exceedingly rare.

So to support built-in numbers you can use just int, float or complex. To handle other ABC's you should use appropriate numbers member. I don't know why float was not made compatible with numbers.Number.

For almost all cases you can use a type alias (TypeAlias was backported with typing_extensions module for python<3.10):

from fractions import Fraction
from numbers import Number
from typing import TypeAlias


AnyNumber: TypeAlias = Number | float

def f(x: AnyNumber) -> bool:
    return x == 0


f(1)
f(1.0)
f(Fraction(1, 3))

This typechecks. One incompatible class I'm aware of is decimal.Decimal: it is not compatible (it would be expected, if Number were made compatible with float, because Decimal is not and Decimal(1) / 2 fails - but it is not the case, as we'll see later).

If your function uses AnyNumber and int together, everything dies:

def f(x: AnyNumber) -> float:
    return x / 2   1  # E: Unsupported operand types for / ("Number" and "int")

Although you can, for example, do Fraction(1,2) / 2, Number does not guarantee int or float compatibility. You can use numbers.Real or numbers.Complex instead - they are compatible with float:

AnyReal: TypeAlias = Real | float

This allows x / 2 1 and remains incompatible with decimal.Decimal, but now it is intended behavior.

You can use this playground to investigate the topic further. Also having look at numbers in typeshed may help.

  • Related