You’re looking for bound:

T = TypeVar('T', bound=Callable)

From the docs:

a type variable may specify an upper bound using bound=<type>. This means that an actual type substituted (explicitly or implicitly) for the type variable must be a subclass of the boundary type, see PEP 484.

TypeVar(name, *args) means that the type has to be one of args, so all instances of T would just be replaceable by Callable if T = TypeVar('T', Callable) were allowed.

You should be able to see the difference here (though I didn’t actually try it, heh):

from typing import Generic, TypeVar, Callable

T = TypeVar('T', Callable, bool)

class Foo(Generic[T]):
    value: T

    def __init__(self, value: T) -> None:
        self.value = value

class Bar:
    baz = 5

    def __call__(self):
        pass

f = Foo(Bar())
print(f.value.baz)  # doesn’t typecheck because f.value is only a Callable
Answer from Ry- on Stack Overflow
🌐
Python documentation
docs.python.org › 3 › library › typing.html
typing — Support for type hints
1 month ago - Using a constrained type variable, however, means that the TypeVar can only ever be solved as being exactly one of the constraints given:
🌐
Gaohongnan
gaohongnan.com › computer_science › type_theory › 05-typevar-bound-constraints.html
Bound and Constraint in Generics and Type Variables — Omniverse
Concrete: T = TypeVar("T", bound=BaseModel) means T can be any type that is a subclass of BaseModel (from pydantic). In Python’s type hinting system, you can define a type variable that restricts which types can be used in place of it by specifying an upper bound.
🌐
Python
peps.python.org › pep-0695
PEP 695 – Type Parameter Syntax | peps.python.org
In the implementation, any expression that is a Tuple AST node is treated as a constraint, and any other expression is treated as a bound. It also modifies existing AST node types FunctionDef, AsyncFunctionDef and ClassDef to include an additional optional attribute called typeparams that includes a list of type parameters associated with the function or class. This PEP introduces three new contexts where expressions may occur that represent static types: TypeVar bounds, TypeVar constraints, and the value of type aliases.
🌐
Python
typing.python.org › en › latest › spec › generics.html
Generics — typing documentation
TypeVar supports constraining parametric types to a fixed set of possible types (note: those types cannot be parameterized by type variables). For example, we can define a type variable that ranges over just str and bytes. By default, a type variable ranges over all possible types.
🌐
Python.org
discuss.python.org › python help
Is it possible to put negative constraints on a TypeVar? - Python Help - Discussions on Python.org
February 1, 2024 - T = TypeVar("T") # T must not be a subtype of list I want to do something like this: class A(Generic[T]): def __init__(self, value: T | list[T]): if isinstance(value, list): "do something" else: "do something else" If T can be a subtype of list, this logic wouldn’t work. If negative type constraint ...
🌐
Python.org
discuss.python.org › typing
Constraining generic argument types - Typing - Discussions on Python.org
June 27, 2024 - Please consider the following code: from typing import Callable, TypeVar _T = TypeVar("_T") def simple(x: _T, y: _T) -> _T: return x def complex(x: Callable[[str], _T], y: _T) -> _T: return y reveal_type(simple(123, "")) reveal_type(complex(int, "")) Running pyright on it returns a type of int | str for both functions, generating a union of the actual argument types. mypy on the other hand is inconsistent here.
🌐
Mypy
mypy.readthedocs.io › en › stable › generics.html
Generics - mypy 1.19.1 documentation
There is also a legacy syntax that relies on TypeVar. Here the number of type arguments must match the number of free type variables in the generic type alias definition. A type variables is free if it’s not a type parameter of a surrounding class or function. Example (following PEP 484: Type aliases, Python 3.11 and earlier):
🌐
GitHub
github.com › python › typing › discussions › 1080
TypeVars with constraints in other languages · python/typing · Discussion #1080
EDIT: Actually, ^that doesn't have exactly the same semantics, because there might be situations where the TypeVar isn't narrowed to either str or bytes, and the inferred type remains str | bytes... with constraints, the TypeVar has to be inferred as either str or bytes; it can't be inferred as the union type.
Author   python
Find elsewhere
🌐
Python.org
discuss.python.org › typing
How to write a function that accepts either a TypeVar or a list of that TypeVar? - Typing - Discussions on Python.org
January 15, 2024 - I’ve got a function where an argument is a dict[str, int | list[int]], and I want to replace int with a type variable. In other words, dict holds non-sequence objects or lists of those objects, but all basal elements are of the same type. V = TypeVar("V") # if we had type negation, would be bound to !list def baz(val: Union[list[V], V]) -> V: if isinstance(val, list): return val[0] return val baz(1) baz([1]) mypy & pyright both point out that, in baz([1]), debugme.py:7: not...
🌐
Python
peps.python.org › pep-0696
PEP 696 – Type Defaults for Type Parameters | peps.python.org
T1 = TypeVar("T1", bound=int) TypeVar("Ok", default=T1, bound=float) # Valid TypeVar("AlsoOk", default=T1, bound=int) # Valid TypeVar("Invalid", default=T1, bound=str) # Invalid: int is not a subtype of str · The constraints of T2 must be a superset of the constraints of T1.
🌐
Python.org
discuss.python.org › python help
Union not compatible with constrained TypeVar - Python Help - Discussions on Python.org
September 9, 2024 - Could someone explain why this does not typecheck? Is this a legit error or a limitation in the type checkers? Is there a way to make this work? from typing import TypeVar, Union class A:... class B:... # A generic function P = TypeVar('P', A, B) def dostuff(value: P) -> P: return value # Another function that returns any of the types that constrain P U = Union[A, B] def make_object() -> U: return A() dostuff(A()) # OK dostuff(B()) # OK obj = make_object() dostuff(obj) # Does not...
🌐
GitHub
github.com › python › typing › issues › 82
Disallow TypeVar with only one constraint · Issue #82 · python/typing
April 14, 2015 - Types like TypeVar('T', Foo) don't make sense since you can always replace them with just Foo. Although type vars with the only constraint don't break anything, they look really con...
Author   vlasovskikh
🌐
GitHub
github.com › python › mypy › issues › 12952
Error creating TypeVar using unpacked types as constraints · Issue #12952 · python/mypy
June 7, 2022 - Unexpected argument to "TypeVar()" [misc] at T definition · Variable "__main__.T" is not valid as a type [valid-type] at is_valid definition If using the explicit int, float type constraints code works as expected. Your Environment · Mypy version used: mypy 0.960 · Python version used: 3.10 ·
Author   LeoCalbi
🌐
GitHub
github.com › python › mypy › issues › 7362
generic subclasss and typevar with constraint · Issue #7362 · python/mypy
August 18, 2019 - import typing T = typing.TypeVar('T', str, int) class Base(typing.Generic[T]): def __init__(self, bar: T): self.bar: T = bar class Child(Base[T]): def __init__(self, bar: T): super().__init__(bar)
Author   ZeeD
Top answer
1 of 2
139

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)
2 of 2
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:

  • python/mypy #2984: List[subclass] is incompatible with List[superclass]
  • python/mypy #7049: [Question] why covariant type variable isn't allowed in instance method parameter?
    • Contains a good example from @Michael0x2a
🌐
Google Groups
groups.google.com › g › comp.lang.python › c › BWiG_jeXetI
TypeVar single constraint not allowed, why?
The documentation for typing.TypeVar gives these two examples: T = TypeVar('T') # Can be anything A = TypeVar('A', str, bytes) # Must be str or bytes I was suprised to find out that the following does not work, exception says that TypeVar is not definable with only one constraint: A = TypeVar('A', type) # Must be a type, like class Foo, etc (rather than an instance of a type) The TypeVar source code explicitely forbids only one type constraint, what is the rationale behind this?
🌐
GitHub
github.com › python › typing › issues › 39
TypeVar example in PEP is confusing · Issue #39 · python/typing
January 15, 2015 - The following type variable constraint example from the PEP is confusing: from typing import Iterable X = TypeVar('X') Y = TypeVar('Y', Iterable[X]) def filter(rule: Callable[[X], bool], input: Y) -> Y: ...
Author   JukkaL
🌐
Python
peps.python.org › pep-0484
PEP 484 – Type Hints - Python Enhancement Proposals
TypeVar supports constraining parametric types to a fixed set of possible types (note: those types cannot be parameterized by type variables). For example, we can define a type variable that ranges over just str and bytes.