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, Unions seem to be how I represent that:

U = Union[int, str] 

U can be an int or a str.

I noticed though that TypeVars 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?

Asked By: Carcigenicate

||

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.

Answered By: Carcigenicate

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
Answered By: S.B