Search code examples
pythonpython-typingmypy

How to define a generic type for real numbers for Python type hinting


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?


Solution

  • 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.