Search code examples
pythongenericstype-hintingmypy

How to pass Union-typed values to generic function without "isinstance"?


I'm confused about how to store generics/unions in collections. Consider T below that can be bool or str. We can represent this as a bound TypeVar or as a Union. The difference matters when semantically requiring that the generic is a single type through multiple namings:

from typing import TypeVar, Union, List, NoReturn

T = TypeVar("T", bool, str)

T_Union = Union[bool, str]

def generic_function(event: T) -> T:
    # must return same type as passed in
    ...

def union_function(event: T_Union) -> T_Union:
    # can return any type in T_Union, not necessarily what was passed in
    ...

Now, if I want to store in a list some valid values that could be passed to these functions, I see that the list type cannot contain an unbound generic, so I store these values in a Union where each element is one of the types of T:

unbound_list_of_event_types: List[T] = [True, "foo"]  # error: Type variable "__main__.T" is unbound
list_of_event_types: List[T_Union] = [True, "foo"]

But, I cannot call the generic function with the Union-typed value:

generic_function(list_of_event_types[0])  # error: Value of type variable "T" of "generic_function" cannot be "Union[bool, str]"
union_function(list_of_event_types[0])

This strikes me as odd, because if we exhaustively check the types in the Union, every function call takes the same form, which looks like needless isinstance-checking:

def assert_never(value: NoReturn) -> NoReturn: ...

# Exhaustively checking the types and calling the function with the *same signature*
if isinstance(list_of_event_types[0], bool):
    generic_function(list_of_event_types[0])
elif isinstance(list_of_event_types[0], str):
    generic_function(list_of_event_types[0])
else:
    assert_never(list_of_event_types[0])

# Seems redundant when we could do this:
generic_function(list_of_event_types[0])  # type: ignore[type-var]

https://mypy-play.net/?mypy=0.931&python=3.10&gist=6133504b68fa1e74e844d0fc280ee42f

Is there a better way to store these values in a collection so they can be passed to generic functions?


Simon Hawe's suggestion to use Union allows this to type check:

T2 = TypeVar("T2", bound=Union[bool, str])

def generic_union_bound_function(event: T2) -> T2:
    # must return same type as passed in
    ...

list_of_event_types: List[Union[bool, str]] = [True, "foo"]

reveal_type(generic_union_bound_function(list_of_event_types[0]))  # bool | str
reveal_type(generic_union_bound_function(True))  # bool

But what if we want to use Type[T] to deduce the generic? (this example more closely looks like callback registration for various event types). I opened a mypy bug for this case: https://github.com/python/mypy/issues/12115


Solution

  • The problem here is that list[Union[t1,t2]] means that the type is either t1 or t2 or t1 and t2. t1 and t2 would not be a valid type for something typed with TypeVar('T', t1, t2), as this mean 't1' or 't2' but not both. In your example of bool and str, there is probably nothing that is both bool and str, but in general, you could have something like

    class X:
        pass
    
    class Y:
       pass
    
    class XY(X,Y):
       pass
    
    T_U = Union[X,Y]
    
    x : T_U = X()
    y : T_U = Y()
    xy : T_U = XY()
    

    Now, what you could use for your example would be a TypeVar that is bound to the Union of bool and str. If you type a function with that, it would still resolve to input type is equal to the output type. It would additionally allow the potential type that is a type of both types from the union. This, however, is anyways not really possible for bool and str. So if you try that code

    T = TypeVar("T", bound= Union[bool, str])
    def some_function(e:TR) -> TR:
        ...
        
    a = some_function"a")
    b = some_function(1)
    
    d = a.split() # works
    c = b.split() # works error: "int" has no attribute "split"
    

    it will exactly check for what you wanted.