I am trying to define a custom type that accepts all real numbers (for Python type hinting). Actually, I would like to support any number type that allows meaningful comparison, but sticking to just the real numbers is good enough. However, the way I am doing it does not seem to be correct. For instance, with the following snippet:
import numbers
import typing
MyDataType = typing.NewType('MyDataType', numbers.Real)
def f(a : MyDataType) -> None:
pass
f(5)
I get mypy complaining that:
Argument 1 to "f" has incompatible type "int"; expected "MyDataType"
How do I actually achieve what I am trying to do?
The classes in the numbers
package are Abstract Base Classes (ABCs) (i.e. cannot be instantiated), and the types int
and float
are registered as virtual subclasses of numbers.Integral
and numbers.Real
respectively. Registering a virtual subclass allows it to be read by isinstance
and issubclass
(Read more about ABCs in the docs).
>>> issubclass(int, numbers.Real)
True
>>> issubclass(float, numbers.Real)
True
However, since int
and float
are registered as virtual subclasses, mypy
is currently unable to resolve int
and float
as valid subclasses of numbers.Real
, as seen by accessing cls.__mro__
:
>>> int.__mro__
(<class 'int'>, <class 'object'>)
>>> float.__mro__
(<class 'float'>, <class 'object'>)
>>> numbers.Integral.__mro__
(<class 'numbers.Integral'>, <class 'numbers.Rational'>, <class 'numbers.Real'>, <class 'numbers.Complex'>, <class 'numbers.Number'>, <class 'object'>)
>>> numbers.Real.__mro__
(<class 'numbers.Real'>, <class 'numbers.Complex'>, <class 'numbers.Number'>, <class 'object'>)
After reading through a solid chunk of the mypy
code, it seems there might not be an implementation for checking if types are a valid virtual subclass. Theoretically, mypy
should recognize int
and float
as valid subclasses, and the answer to your other question of allowing for any subtype could be done using the bound
keyword from TypeVar
.
import numbers
import typing
MyDataType = typing.TypeVar('MyDataType', bound=numbers.Real)
def f(a: MyDataType) -> None:
pass
f(5) # in reality, mypy returns a type-var error since it cannot resolve "int" as a valid subclass of "numbers.Real"
To use int
and float
types, including any other types, add each type as an argument of TypeVar
.
import typing
MyDataType = typing.TypeVar('MyDataType', int, float)
def f(a: MyDataType) -> None:
pass
f(5) # Success: no issues found in 1 source file
Alternatively, if you're using Python 3.12, you can now use the following syntax:
def f[T: int, float](a: T) -> None:
pass
f(5) # Success: no issues found in 1 source file
If you want to use this syntax, make sure to include the flag --enable-incomplete-feature=NewGenericSyntax
when running mypy
.