Tuple with multiple numbers of arbitrary but equal type
Asked Answered
N

1

10

Currently, I am checking for tuples with multiple (e.g. three) numbers of arbitrary but equal type in the following form:

from typing import Tuple, Union

Union[Tuple[int, int, int], Tuple[float, float, float]]

I want to make this check more generic, also allowing numpy number types. I.e. I tried to use numbers.Number:

from numbers import Number
from typing import Tuple

Tuple[Number, Number, Number]

The above snipped also allows tuples of mixed types as long as everything is a number.

I'd like to restrict the tuple to numbers of equal type.

How can this be achieved?


Technically, this question applies to Python and the type hints specification itself. However, as pointed out in the comments, its handling is implementation specific, i.e. MyPy will not catch every edge case and/or inconsistency. Personally, I am using run-time checks with typeguard for testing and deactivate them entirely in production.

Nystatin answered 20/9, 2021 at 11:54 Comment(9)
That's a bit tricky. I tried something with a TypeVar but then realized that np.floating and np.integer are floats/ints, i.e. isinstance(np.float64(3), float) -> True.Spirituous
Even in your existing annotation, MyPy will sadly fail to raise an error if you pass in a tuple in which some items are floats and some are ints mypy-play.net/…Middelburg
@Spirituous Numpy has a similar fundamental class like the regular Python standard library: np.number. Besides, a numpy.float64 is - under most circumstances, depending on configuration, platform and compiler options (sys.float_info) - literally identical to a Python float, so this kind of thing would actually be ok for me.Nystatin
@AlexWaygood Wow, that's impressive. Well, I am doing run-time checking, i.e. while my tests are running, I am using typeguard for verification instead of MyPy. It is able to catch this kind of inconsistencies.Nystatin
I see. It might be good to edit that into your question — I assumed you were using your type hints for static analysis, as that's the most common use. I can't help you with respect to typeguard, as I don't have the package installed, and it sounds as though it interprets the semantics of type annotations somewhat differently to mypy et al.Middelburg
@AlexWaygood I added a comment to the question as suggested. I hope it helps. Static analysis does not tell me everything I want to know. I am torturing my code with pytest and hypothesis plus run-time checks.Nystatin
Much appreciated!Middelburg
@Nystatin Can you explain why T = TypeVar('T', int, float); Tuple[T, T, T] does not work for you? It should be equivalent to your "manual" versions Tuple[int, int, int] and Tuple[float, float, float], so if these manual versions work for you, so should the TypeVar version.Stander
@Stander It does indeed work. I was not aware of TypeVar, to be honest. Thanks!Nystatin
G
9

You can use TypeVar with bound argument. It allows restricting types to subtypes of a given type. In your case, the types should be the subtypes of Number:

from numbers import Number
from typing import TypeVar
 
T = TypeVar('T', bound=Number)
Tuple[T, T, T]

Why does it work?

TypeVar is a variable that allows to use a particular type several times in type signatures. The simplest example:

from typing import TypeVar, Tuple

T = TypeVar('T')
R = TypeVar('R')

def swap(x: T, y: R) -> Tuple[R, T]:
    return y, x

The static type checker will infer that arguments of the function swap should be the same as outputs in reversed order (note that return type T will be the same as the input type T).

Next, it might be useful to introduce restrictions for TypeVar. For instance, one could restrict values of a type variable to specific types: TypeVar('T', int, str). In this answer, we use another kind of restriction with a keyword bound – it checks if the values of the type variable are subtypes of a given type (in our case, Number, which is a base class for all numerical types in Python).

More examples: mypy documentation and PEP 484.


Working test:

from numbers import Number
from typing import Tuple, TypeVar

import numpy as np
from typeguard import typechecked
 
T = TypeVar('T', bound=Number)

@typechecked
def foo(data: Tuple[T, T, T]):
    print('expected OK:', data)
    
for data in (
    (1, 2, 3), # ok
    (1.0, 2.0, 3.0), # ok
    (1, 2.0, 3), # TypeError
    (1.0, 2.0, 3), # TypeError
    (1.0, 2.0, np.float64(3.0)), # ok (yes!)
    (1, 2.0, np.float32(3)), # TypeError
    (1, 2.0, np.uint8(3)), # TypeError
):
    try:
        foo( data )
    except TypeError:
        print('expected TypeError:', data)
Griner answered 23/9, 2021 at 12:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.