Skip to content

← Orientation and the Gate step 8 of 13

Medium Primitives

Placement 1/6: generics and variance

Placement diagnostic, 1 of 6. These six short problems exist to find out what you already know so the course can skip what you do not need. Aim for about seven minutes each. There is no partial credit and no worked example — if a problem is opaque, that is the signal, and its track is where you start.


This one is about variance: the rule that decides whether a Frame[int] may be handed to a function that asked for a Frame[object].

The starter has a PEP 695 generic class and a caller that will not compile:

error: Argument 1 to "summarise" has incompatible type "Frame[int]";
    expected "Frame[object]"  [arg-type]

Every int is an object, so the rejection feels wrong. It is not. Under PEP 695 you do not write covariant=True anywhere; mypy infers the variance of T from how the class body uses it, and it infers invariant the moment T appears anywhere writable. An invariant Frame[int] is not a subtype of Frame[object], and the reason is a real bug the checker is preventing: if it were allowed, summarise — holding what it believes is a Frame[object] — could push a str into your Frame[int].

Fix the class so Frame is covariant in T, without touching summarise or solve.

def solve(ints: list[int], names: list[str]) -> list[str]:

It builds a Frame[int] and a Frame[str], passes each to summarise, and concatenates the results. summarise renders each row as f"{i}:{row!r}" with i restarting from 0 for each frame — note !r, so the string "0" renders as 0:'0' and the integer 0 renders as 0:0.

The part that catches people

Deleting append is necessary but not sufficient. self._rows: list[T] is still an invariant position all by itself, because list is itself invariant in its element type — a list[int] is not a list[object] for the same reason a Frame[int] is not a Frame[object]. Variance is inferred from the declared types of the attributes, not from whether you happen to mutate them, and the leading underscore buys you nothing: mypy has no notion of “private”.

To be covariant, T must only ever appear in positions you can read out of: return types, and attributes whose own type is immutable in T.

Production consequence

This is the entire reason your codebase is full of Sequence[Foo] and Mapping[str, Foo] parameters instead of list[Foo] and dict[str, Foo]. Sequence is covariant, so a caller with a list[Dog] can satisfy a Sequence[Animal] parameter. list is invariant, so the same caller cannot satisfy a list[Animal] parameter and has to build a copy — or, far more often, someone reaches for cast and the type system stops being evidence. Choosing an immutable internal representation is a design decision that buys you a wider public interface.