Assuming I want to write a function that accepts any type of number in Python, I can annotate it as follows:
from numbers import Number
def foo(bar: Number):
print(bar)
Taking this concept one step further, I am writing functions which accept number types, i.e. int, float or numpy dtypes, as arguments. Currently, I am writing:
from typing import Type
def foo(bar: Type):
assert issubclass(bar, Number)
print(bar)
I thought I could substitute Type with something like NumberType (similar to NotImplementedType and friends, re-introduced in Python 3.10), because all number types are subclasses of Number:
from numbers import Number
import numpy as np
assert issubclass(int, Number)
assert issubclass(np.uint8, Number)
As it turns out (or at least as far as I can tell), there is no such thing as a generic NumberType in Python (3.9):
>>> type(Number)
abc.ABCMeta
Is there a clean way (i.e. without runtime checks) to achieve the desired kind of annotation?
There is no general way to do this. Numbers are not strictly related to begin with and their types are even less.
While numbers.Number might seem like "the type of numbers" it is not universal. For example, decimal.Decimal is explicitly not a numbers.Number as either subclass, subtype or virtual subclass. Specifically for typing, numbers.Number is not endorsed by PEP 484 -- Type Hints.
In order to meaningfully type hint "numbers", one has to explicitly define what numbers are in that context. This might be a pre-existing numeric type set such as int <: float <: complex, a typing.Union/TypeVar of numeric types, a typing.Protocol to define operations and algebraic structure, or similar.
from typing import TypeVar
from decimal import Decimal
from fractions import Fraction
#: typevar of rational numbers if we squint real hard
Q = TypeVar("Q", float, Decimal, Fraction)
All that said, "the type of the type of numbers" is even less meaningful. Even the specific numbers.Number has practically no features at all: it cannot be converted to a concrete type, nor can it be instantiated to a meaningful number.
Instead, use "the type of some type of numbers":
from typing import Type
def zero(t: Type[Q]) -> Q:
return t() # all Type[Q]s can be instantiated without arguments
print(zero(Fraction))
If the only goal of the Type is to create instances, it can be better to request a Callable instead. This covers types as well as factory functions.
def one(t: Callable[[int], Q]) -> Q:
return t(1)
This is not quite an answer to the original question. (Alex Waygood's answer is properly selected as responsive.) However, I have attempted to generalize my own work-arounds for the sharp edges between numbers and typing in Python. Those work-arounds now live in numerary (having extracted it via c-section from dyce, where it was conceived).
I didn't spend a lot of time on naming, in the hopes it will be short-lived. Docs are online. It should be considered experimental, but it is rapidly approaching stability. Feedback, suggestions, and contributions are desperately appreciated.