skip to content

What does TypeVar("T_co", covariant=True) change about a Generic class using it?

level: seniorimportance: nice to knowfreq 20%

answer

  1. The default is neither direction
  2. The flag buys assignability and costs freedom
  3. Output positions only, or input positions only
  4. A suffix that tells readers the direction
  5. covariant=True plus the _co naming convention

basics

~20 s

It makes the class covariant in that parameter, so Box[Dog] is usable where Box[Animal] is expected. In exchange the checker requires the parameter to appear only in output positions; using it as a method parameter is an error.

solid answer

~40 s

A `typing.TypeVar` is invariant unless you say otherwise. Passing `covariant=True` tells the checker that `Box[Dog]` is a subtype of `Box[Animal]`, which is sound only if the class never *consumes* a value of that type — so the checker enforces that the variable appears only in return types and read-only properties, and reports an error if you use it as a method parameter. `contravariant=True` is the mirror: `Sink[Animal]` becomes usable as a `Sink[Dog]`, and the variable may appear only in parameter positions. The `_co` and `_contra` name suffixes are a strong community convention, not a language rule — they exist because a reader of `class Box(Generic[T_co])` can then see the variance without opening the `TypeVar` declaration. Declare variance only for genuinely producer-only or consumer-only classes; invariance is the correct default.

code

python · 16 lines
python
from typing import Generic, TypeVar

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

class Box(Generic[T_co]):
    def __init__(self, item: T_co) -> None:
        self._item = item

    def get(self) -> T_co:
        return self._item

class Animal: ...
class Dog(Animal): ...

box: Box[Animal] = Box(Dog())
print(type(box.get()).__name__)

go deeper

for a junior

You will mostly consume generic classes rather than declare them. Recall only that a type variable is invariant unless its declaration explicitly says covariant or contravariant, and that the _co suffix is a hint about which.

for a middle

Explain what the flag buys and what it costs: assignability between parameterised versions of the class, in exchange for restricting the variable to output positions only, or input positions only.

for a senior

Show judgement about when to declare it at all. Justify covariance from a read-only design, explain the checker error when the variable reaches a parameter position, and say why most classes should stay invariant.

for a principal

Own it as a library-design decision. Variance is part of a published type contract: it widens what callers may assign, constrains every future method you add, and is far harder to remove than to introduce.

### What the flag does `typing.TypeVar` accepts two mutually exclusive keyword flags, `covariant` and `contravariant`, both defaulting to false. With both false the type variable is **invariant**, which is the right answer for most classes and the reason `list` behaves as it does. Set `covariant=True` and the checker will treat `Box[Dog]` as a subtype of `Box[Animal]` wherever `Box` is generic in that variable. Set `contravariant=True` and the relation reverses: `Sink[Animal]` becomes usable where `Sink[Dog]` is expected. ### The obligation that comes with it Variance is a claim about how the class can be used, and the checker holds you to it. A covariant variable may appear only in **output** positions: - return types of methods - read-only property return types - the element type of another covariant generic used in an output position The moment a covariant variable appears as a method parameter, the checker reports an error along the lines of *cannot use a covariant type variable as a parameter*. That error is not pedantry; it is the `append`-a-`Cat` unsoundness caught at the declaration site rather than at every call site. Contravariance imposes the mirror obligation: the variable may appear in parameter positions and not in return types. This is why almost every mutable container you write should keep the default invariance. If your class has both a getter and a setter for the same type, no variance annotation is available to you, and trying to force one just moves the error around. ### Naming: `_co` and `_contra` By convention a covariant type variable is named with a `_co` suffix and a contravariant one with `_contra`. The convention exists because variance is declared in one place and *used* in another. A reader who encounters ```python class Registry(Generic[T_co]): ... ``` learns immediately that `Registry` is a producer and that `Registry[Dog]` will be accepted where `Registry[Animal]` is wanted, without scrolling up to the `TypeVar` call. The standard library's own stubs follow the same convention throughout. Nothing in the interpreter or the checker enforces it — a covariant variable called `T` type-checks identically — but dropping it removes a signal that costs one line to keep. ### When to reach for it The honest answer is: rarely, and only for genuinely single-direction abstractions. - **Covariant** suits read-only containers and result wrappers: a frozen collection, a lazily-computed result holder, an event source that only yields values. If you can describe the class as "a thing you get values out of", covariance is probably right. - **Contravariant** suits pure consumers: a writer, a validator, a serializer sink, a comparison key function holder. "A thing you put values into." - **Invariant** — the default — suits everything else, which is most classes. A useful sanity check before adding the flag: write down the two or three call sites that will benefit. If you cannot name one, the variance annotation is a decoration that constrains your future methods for nothing. Adding it later is a small change; removing it after callers depend on the assignability is not. ### The runtime side As with every part of the typing system, the flags do nothing at runtime. `Box(Dog())` builds an ordinary object; the covariance exists purely for static analysis and for readers. A checker run takes seconds and catches the variance mistake at the declaration; a test suite that runs for 27 minutes will happily never exercise the one path where the substitution matters. ### Version notes The explicit `covariant=True` / `contravariant=True` flags on `typing.TypeVar` are the classic spelling and remain fully supported on Python 3.14. Since Python 3.12 the newer type-parameter syntax declares generic classes without a separate `TypeVar` call, and variance for those parameters is determined by the checker from how the parameter is used rather than by a keyword flag — the details of that inference belong with that syntax. Code that still writes `TypeVar("T_co", covariant=True)`, including a great deal of published stub material, works unchanged. ### Reading someone else's declaration Most engineers meet these flags while reading a library's types rather than while writing their own, and the reading skill is worth practising. Seeing `Generic[T_co]` in a class header tells you three things at once: parameterised versions of that class are assignable upward, so a value of the subclass-parameterised type will be accepted where the base-parameterised one is expected; the class is a producer, so expect getters and properties rather than setters; and any method you were hoping to find that *takes* a `T_co` almost certainly does not exist, because the checker would not have permitted it. `T_contra` tells you the mirror story — a sink with `send`- or `write`-shaped methods and no way to read a value back out. A plain `T` tells you the class does both, and that you will have to match the parameter exactly at every call site.

  • Why does a checker error when a covariant type variable is used as a method parameter?
    Because covariance promises that `Box[Dog]` is usable as a `Box[Animal]`, and a method that *accepts* that type would then let a caller pass a `Cat` into a box of dogs — the same unsoundness that makes `list` invariant. Restricting the variable to return types and read-only properties is what keeps the promise true.
  • Do the _co and _contra name suffixes affect anything the checker does?
    No. They are purely a naming convention. The variance comes from the keyword flag on the `TypeVar` call; the suffix exists so a reader of the class header can see the direction without hunting for the declaration. The standard library stubs follow it, and dropping it costs readability rather than correctness.
  • When should a class keep the default invariance?
    Whenever the type parameter appears in both an input and an output position — that is, any container or wrapper with both a getter and a setter for the same type. There is no sound variance annotation for that shape, and invariance is also the right default when you cannot name a call site that the extra assignability would unblock.

Declaring covariance is like stamping a container "outlet only": it buys you the right to hand it to anyone expecting a broader outlet, at the price of never being allowed to fit an inlet valve.

saying these in an interview costs you the question

  • Thinks a TypeVar is covariant by default
  • Adds covariant=True to silence an assignability error
  • Uses a covariant variable as a method parameter
  • Believes the _co suffix itself sets the variance
  • Declares a container covariant despite having a setter
  • Sets both covariant and contravariant on one TypeVar

context