0

I have a Python function used as a runtime check that ensures an object is an instance of at least one class in a tuple. It works and my type-checker (pyright) correctly infers the return type from the arguments:

from typing import Union, Type, Tuple, Any, TypeVar

T = TypeVar('T')

def inst(obj: Any, type: Union[Type[T], Tuple[Type[T], ...]]) -> T:
    if isinstance(obj, type):
        return obj
    raise Exception(f'{obj} must be an instance of {type}')

x = inst(3, (int, str))  # => pyright correctly says x is `int | str`

However, when I attempt to factor out the pattern Union[Type[T], Tuple[T, ...]] into a type alias, it breaks type inference:

from typing import Union, Type, Tuple, Any, TypeVar

T = TypeVar('T')
TypeOrTupleOfTypes = Union[T, Tuple[T, ...]]

def inst(obj: Any, type: TypeOrTupleOfTypes[Type[T]]) -> T:
    if isinstance(obj, type):
        return obj
    raise Exception(f'{obj} must be an instance of {type}')

x = inst(3, (int, str))  # => pyright now thinks x is `tuple[Type[int], Type[str]]`

Why can't I use a type alias here?

Sean Mackesey
  • 10,701
  • 11
  • 40
  • 66
  • Your first one isn't correct either... `Tuple[Type[T], ...]])` implies a tuple *all of the exact same type*. So only `(int, int)` would be valid, `(int, str)` would not – juanpa.arrivillaga Dec 07 '21 at 04:58
  • Interesting to note, typeshed uses `Tuple[Any, ...]`, there is a comment about needing to use that until python (pretty much mypy, I would say) supports recursive types: https://github.com/python/typeshed/blob/master/stdlib/builtins.pyi#L1097 I believe pyright supports recursive types, though – juanpa.arrivillaga Dec 07 '21 at 05:11
  • @juanpa.arrivillaga Please try out the code before saying it is incorrect. It works. The reason is that type `T` matches `Union[Type[int], Type[str]]`, and `Tuple[Union[Type[int], Type[str]], ...]` matches the value `(int, str)`. Not sure if that is true of `mypy` but it is true in `pyright`. – Sean Mackesey Dec 07 '21 at 16:21
  • Yes, I understand it "works" it is still wrong. I highly suspect this works due to pyright's aggressive type narrowing with `isinstance`, but try a simple example, something like `def foo(klass: Type[T]) -> Tuple[T, ...]:` then in the body, maybe something like `x: T = klass(1)` and `y: str = "foo"` then `return x, y`, you will see that PyRight correctly complains `error: Expression of type "tuple[T@foo, Literal['foo']]" cannot be assigned to return type "Tuple[T@foo, ...]"` – juanpa.arrivillaga Dec 07 '21 at 19:28
  • I don't follow you-- you provided a different example that produces a type error. But in what sense is my code wrong? It is not incorrect because it exploits information gained from `isinstance`. The function does what it is supposed to do, and the return type is correctly inferred from the provided tuple. I would agree my code is "wrong" if one of these breaks with some otherwise valid input, but I haven't seen that. Can you provide an example where the type inference does not work correctly for my `inst` function? – Sean Mackesey Dec 07 '21 at 23:59

0 Answers0