Reputation: 265
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
Upvotes: 1
Views: 1187
Reputation: 4529
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.
Upvotes: 1