47

I'm struggling to get my head around the difference between the following two TypeVars:

from typing import TypeVar, Union

class A: pass
class B: pass

T = TypeVar("T", A, B)
T = TypeVar("T", bound=Union[A, B])

Anyone want to enlighten me?


As an example of something I don't get: this passes type checking...

T = TypeVar("T", bound=Union[A, B])

class AA(A):
    pass


class X(Generic[T]):
    pass


class XA(X[A]):
    pass


class XAA(X[AA]):
    pass

...but with T = TypeVar("T", A, B), it fails with

error: Value of type variable "T" of "X" cannot be "AA"


Related: this question on the difference between Union[A, B] and TypeVar("T", A, B).

2
  • @Carcigenicate -- Regarding your first comment, the type checker always does subtype checking, no matter what kind of TypeVar you're using or whether or not you're using generics. This is actually what pretty much all type systems with nominal subtyping will do -- for example, see Java and C++. The reason your example doesn't work is because while MyUnion may be a subtype of Union[int, str], it isn't a subtype of int. Jan 27, 2020 at 19:46
  • Regarding your third comment, Union[A, B] is a valid bound according to PEP 484 since that type does not contain any type variables -- a type variable is a type created by using TypeVar. So for example, if you did T1 = TypeVar('T1'), it would then be illegal to try and use T1 within another TypeVar definition by doing either T2 = TypeVar('T2', bound=T2) or T3 = TypeVar('T3', T2, int). This restriction exists mostly so type checkers wouldn't need to implement higher-order types, which is a pretty complex type system feature. Jan 27, 2020 at 19:48

2 Answers 2

65

When you do T = TypeVar("T", bound=Union[A, B]), you are saying T can be bound to either Union[A, B] or any subtype of Union[A, B]. It's upper-bounded to the union.

So for example, if you had a function of type def f(x: T) -> T, it would be legal to pass in values of any of the following types:

  1. Union[A, B] (or a union of any subtypes of A and B such as Union[A, BChild])
  2. A (or any subtype of A)
  3. B (or any subtype of B)

This is how generics behave in most programming languages: they let you impose a single upper bound.


But when you do T = TypeVar("T", A, B), you are basically saying T must be either upper-bounded by A or upper-bounded by B. That is, instead of establishing a single upper-bound, you get to establish multiple!

So this means while it would be legal to pass in values of either types A or B into f, it would not be legal to pass in Union[A, B] since the union is neither upper-bounded by A nor B.


So for example, suppose you had a iterable that could contain either ints or strs.

If you want this iterable to contain any arbitrary mixture of ints or strs, you only need a single upper-bound of a Union[int, str]. For example:

from typing import TypeVar, Union, List, Iterable

mix1: List[Union[int, str]] = [1, "a", 3]
mix2: List[Union[int, str]] = [4, "x", "y"]
all_ints = [1, 2, 3]
all_strs = ["a", "b", "c"]


T1 = TypeVar('T1', bound=Union[int, str])

def concat1(x: Iterable[T1], y: Iterable[T1]) -> List[T1]:
    out: List[T1] = []
    out.extend(x)
    out.extend(y)
    return out

# Type checks
a1 = concat1(mix1, mix2)

# Also type checks (though your type checker may need a hint to deduce
# you really do want a union)
a2: List[Union[int, str]] = concat1(all_ints, all_strs)

# Also type checks
a3 = concat1(all_strs, all_strs)

In contrast, if you want to enforce that the function will accept either a list of all ints or all strs but never a mixture of either, you'll need multiple upper bounds.

T2 = TypeVar('T2', int, str)

def concat2(x: Iterable[T2], y: Iterable[T2]) -> List[T2]:
    out: List[T2] = []
    out.extend(x)
    out.extend(y)
    return out

# Does NOT type check
b1 = concat2(mix1, mix2)

# Also does NOT type check
b2 = concat2(all_ints, all_strs)

# But this type checks
b3 = concat2(all_ints, all_ints)
7
  • Thanks for the answer. There's still bits I don't get. I've added an example to the question which I can't resolve from the info you've given
    – joel
    Jan 27, 2020 at 22:05
  • @JoelB -- I'm not able to reproduce the error you're getting in your new example, at least when using mypy 0.761. I'm happy to take a second look if you update the example to be a more complete repro. Jan 27, 2020 at 22:32
  • I'm using the same mypy version with python 3.6. what python version are you using?
    – joel
    Jan 27, 2020 at 22:35
  • @JoelB -- I'm using Python 3.7, but switching to Python 3.6 doesn't seem to make a difference -- for example, see mypy-play.net/…. Jan 27, 2020 at 22:42
  • 1
    yeah i'm seeing the error there for 3.6 and 3.7. The error's only for TypeVar("T", A, B)
    – joel
    Jan 27, 2020 at 23:46