62

What's 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])

Here's 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).

joel
  • 6,359
  • 2
  • 30
  • 55
  • @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`. – Michael0x2a Jan 27 '20 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. – Michael0x2a Jan 27 '20 at 19:48
  • There is also third option: `T = TypeVar("T", Union[A, B])` – pabouk - Ukraine stay strong Jul 29 '23 at 08:07
  • 1
    @pabouk-Ukrainestaystrong a single constraint isn't allowed by mypy – joel Jul 29 '23 at 09:37

2 Answers2

88

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)
Michael0x2a
  • 58,192
  • 30
  • 175
  • 224
  • 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 '20 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. – Michael0x2a Jan 27 '20 at 22:32
  • I'm using the same mypy version with python 3.6. what python version are you using? – joel Jan 27 '20 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 https://mypy-play.net/?mypy=0.761&python=3.6&gist=73d96dee6e7ffc814d7deb7ec59e32bd. – Michael0x2a Jan 27 '20 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 '20 at 23:46
  • @Michael0x2a I am hitting this same problem, see this: https://mypy-play.net/?mypy=0.761&python=3.6&gist=10e716a7acca275210f59228ff237108 – Intrastellar Explorer Apr 21 '20 at 15:44
  • If I have an abstract class `Base`, can I create a `TypeVar` that is bound to any one of its children classes, but not their union, without explicitly typing them (so that when more child classes are added, I do not need to update the `TypeVar`)? – Jackson H Jun 30 '22 at 19:11
  • What a fantastic answer! – Yonatan Feb 10 '23 at 18:06
  • I filed a related issue comment here: https://github.com/microsoft/pyright/issues/744#issuecomment-1627773919 – nh2 Jul 09 '23 at 17:05
6

After a bunch of reading, I believe mypy correctly raises the type-var error in the OP's question:

generics.py:31: error: Value of type variable "T" of "X" cannot be "AA"

See the below explanation.


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

I think @Michael0x2a's answer does a great job of describing what's happening.


First Case: TypeVar("T", A, B)

The reason boils down to Liskov Substitution Principle (LSP), also known as behavioral subtyping. Explaining this is outside the scope of this answer, you will need to read up on + understanding the meaning of invariance vs covariance.

From python's typing docs for TypeVar:

By default type variables are invariant.

Based on this information, T = TypeVar("T", A, B) means type variable T has value restrictions of classes A and B, but because it's invariant... it only accepts those two (and not any child classes of A or B).

Thus, when passed AA, mypy correctly raises a type-var error.


You might then say: well, doesn't AA properly match behavioral subtyping of A? And in my opinion, you would be correct.

Why? Because one can properly substitute out and A with AA, and the behavior of the program would be unchanged.

However, because mypy is a static type checker, mypy can't figure this out (it can't check runtime behavior). One has to state the covariance explicitly, via the syntax covariant=True.

Also note: when specifying a covariant TypeVar, one should use the suffix _co in type variable names. This is documented in PEP 484 here.

from typing import TypeVar, Generic

class A: pass
class AA(A): pass

T_co = TypeVar("T_co", AA, A, covariant=True)

class X(Generic[T_co]): pass

class XA(X[A]): pass
class XAA(X[AA]): pass

Output: Success: no issues found in 1 source file


So, what should you do?

I would use TypeVar("T", bound=Union[A, B]), since:

  • A and B aren't related
  • You want their subclasses to be allowed

Further reading on LSP-related issues in mypy:

Neuron
  • 5,141
  • 5
  • 38
  • 59
Intrastellar Explorer
  • 3,005
  • 9
  • 52
  • 119
  • When i use `T = TypeVar("T", A, B)` and put a subclass of `A` in a function where type `T` is expected, `mypy --strict` does not show an error. Also, in the python documentation ( https://docs.python.org/3/library/typing.html#typing.TypeVar ) it says `Also note that if the arguments are instances of some subclass of str, the return type is still plain str.` which seems like subtypes are okay for their example, which is the same as the one I am talking about. Anyone knows why? – xuiqzy Oct 20 '20 at 12:20
  • I am not sure @xuiqzy, would you be able to somehow share a minimal repro (ex: via GitHub Gist)? – Intrastellar Explorer Oct 20 '20 at 16:53
  • Looking at the mypy source code shows that subtypes are allowed. – ktb Jan 11 '22 at 03:11
  • `T_co = TypeVar("T_co", AA, A)` passes mypy check just as well. Tested with mypy 0.971 (on python 3.9 and 3.10 both) – bravmi Aug 25 '22 at 08:57