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 typefloat
, an argument of typeint
is acceptable; similar, for an argument annotated as having typecomplex
, arguments of typefloat
orint
are acceptable. This does not handle classes implementing the corresponding ABCs or thefractions.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.