Skip to content

← Getting Started with Tests and Type Checks step 8 of 13

Medium Primitives

Placement 1/6: Make a Generic Class Read-Only

Solve this without the hints and you can skim the type hints track.

Frame[T] is a generic class. T is a type parameter, a placeholder for the type of the rows, so a Frame[int] holds integers and a Frame[str] holds strings. The function summarise takes a Frame[object] and only reads it. Every int is an object, so int is a subtype of object. You would expect summarise to accept a Frame[int], but mypy rejects the call:

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

The reason is append. If summarise could treat a Frame[int] as a Frame[object], it could append a string to a frame of integers. So mypy makes Frame invariant, which means Frame[int] and Frame[object] are unrelated types. Your job is to make it covariant. Then, because int is a subtype of object, Frame[int] is a subtype of Frame[object]. mypy can make a class covariant only when nothing can put a new T into it once the constructor has run.

What to write

Change only the class Frame. Leave summarise and solve as they are, because they are the caller mypy must accept.

  • Keep the class Frame[T] syntax. With it, mypy works out the variance of T from how the class uses T.
  • Remove the append method.
  • The constructor still takes a Sequence[T]. Store the rows in self._rows: tuple[T, ...], a tuple of any length whose items are all T. Keep their order and any duplicates.
  • rows() returns that tuple.
  • Copy the input. If the caller changes their list after building the frame, the frame stays the same.
  • Keep T throughout. Don't replace it with Any or object, and don't silence the error with # type: ignore or cast.

Keep the leading underscore in _rows. A list keeps T invariant because it can change in place. So does a public attribute, because any caller can reassign it. mypy treats an attribute whose name starts with _ as private, and ignores outside writes to it when it works out the variance.

What the driver reports

solve(ints, names) builds a Frame[int] and a Frame[str] and passes each one to summarise. It returns the integer frame's lines, then the string frame's. Each line is "index:value". The index restarts at 0 for each frame, and the value is repr(row), so strings keep their quotes.

solve([1, 2], ["a"])  # ["0:1", "1:2", "0:'a'"]
solve([], [])         # []

The starter already returns these values. It fails only the type check, so mypy grades your change.