We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← The Type System as a Design Tool step 15 of 24
Producer, Consumer, Cell: variance by member set
You do not declare variance under PEP 695. The checker infers it from your method set — which means adding a public mutable attribute to a generic class is a breaking API change, and nothing in the diff says so.
The inference algorithm is short enough to memorise:
- the parameter appears only in return positions -> covariant
- only in parameter positions -> contravariant
- in a public mutable attribute, or in both positions -> invariant
That last clause is the one that bites. A self.items: list[T] is both readable
and writable, and list[T] is itself invariant in T, so a single innocuous
attribute collapses the whole class to invariance. Make it private and expose it
through a read-only @property returning a tuple[T, ...], and covariance
comes back.
This is exactly why ruff marks UP046 and UP047 — the rules that rewrite
Generic[T] classes into PEP 695 syntax — as unsafe fixes. Migrating
T = TypeVar("T", covariant=True) to class C[T] without understanding
inference can silently change your public API’s variance and break downstream
callers who were relying on the old assignability.
And there is no escape hatch through the old spelling: mypy 2.3 rejects
TypeVar(..., infer_variance=True), even though CPython has supported it
since 3.12. PEP 695 syntax is the only route to inferred variance under mypy.
The task
Write three generic classes whose member sets dictate their inferred variance, plus a function that consumes both:
class Producer[T]: # covariant — T only ever comes out
class Consumer[T]: # contravariant — T only ever goes in
class Cell[T]: # invariant — T is a public mutable attribute
def pipe[T](producer: Producer[T], consumer: Consumer[T]) -> None: ...
def solve(names: list[str]) -> tuple[list[str], int, str]: ...
-
Producer[T]is constructed from aSequence[T], stores it privately as atuple[T, ...], and exposes it through a read-onlyitemsproperty. -
Consumer[T]hasput(self, item: T) -> Noneandlog(self) -> list[str]returningstr(item)for each item received in order.Tmust not appear in its attribute types — store the received items aslist[object], or contravariance is lost the same way covariance is. -
Cell[T]has a single publicvalue: Tset in__init__. -
pipeputs every item of the producer into the consumer.
Animal and Dog are given, with Dog a subclass and
__str__ returning "Dog(rex)".
solve builds a Dog per name and returns (consumer log, number of items the producer holds, str of the cell's value).
The variance witnesses are inside solve
These three lines are not decoration — they are the assertions:
producer: Producer[Animal] = Producer(dogs) # Producer[Dog] -> Producer[Animal]
sink: Consumer[Dog] = Consumer[Animal]() # Consumer[Animal] -> Consumer[Dog]
cell: Cell[Animal] = Cell(Animal("origin")) # Cell[Dog] would NOT satisfy this
If Producer is not covariant, line 1 is an assignment error. If Consumer
is not contravariant, line 2 is. Both are load-bearing: your solution does not
type-check unless the member sets are right. pipe(Producer(dogs), sink)
type-checks for the same reason — T solves to Dog, with the
Consumer[Animal] accepted by contravariance.
For Cell, try it yourself: add
a: Cell[Animal] = Cell[Dog](Dog("d"))
b: Cell[Dog] = Cell[Animal](Animal("a"))
and confirm mypy rejects both. A cell you can read and write cannot be substituted in either direction, and seeing both errors is more convincing than reading the rule.
Why contravariance feels backwards
Consumer[Animal] being usable where a Consumer[Dog] is required reads wrong
until you say it out loud: a thing that can accept any animal can certainly
be handed dogs. The substitution goes the other way from the type parameter,
which is precisely what “contravariant” names. Callbacks, sinks, comparators and
handlers all behave this way — see the retry problem for the version of this
that appears in every review of callback-heavy code.
Stuck?
Python reference solution
Sign in to attempt this problem and reveal the reference solution.