What's the difference between a constrained TypeVar and a Union?
Question:
If I want to have a type that can be multiple possible types, Union
s seem to be how I represent that:
U = Union[int, str]
U
can be an int
or a str
.
I noticed though that TypeVar
s allow for optional var-arg arguments that also seem to do the same thing:
T = TypeVar("T", int, str)
Both T
and U
seem to only be allowed to take on the types str
and int
.
What are the differences between these two ways, and when should each be preferred?
Answers:
T
‘s type must be consistent across multiple uses within a given scope, while U
‘s does not.
With a Union
type used as function parameters, the arguments as well as the return type can all be different:
U = Union[int, str]
def union_f(arg1: U, arg2: U) -> U:
return arg1
x = union_f(1, "b") # No error due to different types
x = union_f(1, 2) # Also no error
x = union_f("a", 2) # Also no error
x # And it can't tell in any of the cases if 'x' is an int or string
Compare that to a similar case with a TypeVar
where the argument types must match:
T = TypeVar("T", int, str)
def typevar_f(arg1: T, arg2: T) -> T:
return arg1
y = typevar_f(1, "b") # "Expected type 'int' (matched generic type 'T'), got 'str' instead
y = typevar_f("a", 2) # "Expected type 'str' (matched generic type 'T'), got 'int' instead
y = typevar_f("a", "b") # No error
y # It knows that 'y' is a string
y = typevar_f(1, 2) # No error
y # It knows that 'y' is an int
So, use a TypeVar
if multiple types are allowed, but different usages of T
within a single scope must match each other. Use a Union
if multiple types are allowed, but different usages of U
within a given scope don’t need to match each other.
I want to add, as a consequence of what @Carcigenicate explained:
With Union
, the operation you use between arguments has to be supported by all arguments in any permutation order:
from typing import Union
U = Union[int, str]
def add(a: U, b: U):
return a + b
Here int + int
and str + str
is OK but not the int + str
and str + int
.
Mypy says:
main.py:6: error: Unsupported operand types for + ("int" and "str")
main.py:6: error: Unsupported operand types for + ("str" and "int")
If we change +
to *
: int * str
and str * int
and int * int
is OK but Mypy doesn’t like str * str
:
from typing import Union
U = Union[int, str]
def add(a: U, b: U):
return a * b
Mypy says:
main.py:6: error: Unsupported operand types for * ("str" and "str")
If we change U = Union[int, str]
to U = Union[int, float]
with above tests, it accepts. All four cases are acceptable.
Here we use TypeVar
instead to get rid of those complains, the T
is the same, either int + int
or str + str
:
from typing import TypeVar
T = TypeVar("T", int, str)
def add(a: T, b: T) -> T:
return a + b
If I want to have a type that can be multiple possible types, Union
s seem to be how I represent that:
U = Union[int, str]
U
can be an int
or a str
.
I noticed though that TypeVar
s allow for optional var-arg arguments that also seem to do the same thing:
T = TypeVar("T", int, str)
Both T
and U
seem to only be allowed to take on the types str
and int
.
What are the differences between these two ways, and when should each be preferred?
T
‘s type must be consistent across multiple uses within a given scope, while U
‘s does not.
With a Union
type used as function parameters, the arguments as well as the return type can all be different:
U = Union[int, str]
def union_f(arg1: U, arg2: U) -> U:
return arg1
x = union_f(1, "b") # No error due to different types
x = union_f(1, 2) # Also no error
x = union_f("a", 2) # Also no error
x # And it can't tell in any of the cases if 'x' is an int or string
Compare that to a similar case with a TypeVar
where the argument types must match:
T = TypeVar("T", int, str)
def typevar_f(arg1: T, arg2: T) -> T:
return arg1
y = typevar_f(1, "b") # "Expected type 'int' (matched generic type 'T'), got 'str' instead
y = typevar_f("a", 2) # "Expected type 'str' (matched generic type 'T'), got 'int' instead
y = typevar_f("a", "b") # No error
y # It knows that 'y' is a string
y = typevar_f(1, 2) # No error
y # It knows that 'y' is an int
So, use a TypeVar
if multiple types are allowed, but different usages of T
within a single scope must match each other. Use a Union
if multiple types are allowed, but different usages of U
within a given scope don’t need to match each other.
I want to add, as a consequence of what @Carcigenicate explained:
With Union
, the operation you use between arguments has to be supported by all arguments in any permutation order:
from typing import Union
U = Union[int, str]
def add(a: U, b: U):
return a + b
Here int + int
and str + str
is OK but not the int + str
and str + int
.
Mypy says:
main.py:6: error: Unsupported operand types for + ("int" and "str")
main.py:6: error: Unsupported operand types for + ("str" and "int")
If we change +
to *
: int * str
and str * int
and int * int
is OK but Mypy doesn’t like str * str
:
from typing import Union
U = Union[int, str]
def add(a: U, b: U):
return a * b
Mypy says:
main.py:6: error: Unsupported operand types for * ("str" and "str")
If we change U = Union[int, str]
to U = Union[int, float]
with above tests, it accepts. All four cases are acceptable.
Here we use TypeVar
instead to get rid of those complains, the T
is the same, either int + int
or str + str
:
from typing import TypeVar
T = TypeVar("T", int, str)
def add(a: T, b: T) -> T:
return a + b