Skip to content

← Interfaces with Protocols and ABCs step 8 of 26

Hard Primitives

Fix the Variance of a Serializer and a Parser

You met variance in the type hints track with list[Dog] and with producer and consumer classes. Here the same rule applies to a protocol's type parameter.

A serializer consumes a value and returns a string. A parser takes a string and produces a value. The starter's program already works, and every test passes. mypy still reports five errors, and the first two name the problem:

L7:1 [misc]
Invariant type variable "T" used in protocol where contravariant one is expected

L11:1 [misc]
Invariant type variable "T" used in protocol where covariant one is expected

Why one type variable cannot serve both protocols

The starter declares T = TypeVar("T") once and uses it in both protocols. A plain TypeVar is invariant, but the two protocols need opposite directions:

  • Serializer[T] takes a T in dump(value: T). A serializer that accepts any object can serialize an int, so a Serializer[object] can go where code expects a Serializer[int]. That is contravariance.
  • Parser[T] returns a T from load. Every int is an object, so a Parser[int] can go where code expects a Parser[object]. That is covariance.

The other three errors are in solve, which relies on both substitutions.

What to write

Declare each protocol's type parameter in its class header:

class Serializer[T](Protocol):
    def dump(self, value: T) -> str: ...


class Parser[T](Protocol):
    def load(self, raw: str) -> T: ...

With this syntax, mypy infers each parameter's variance from how the class uses it. Then:

  • Delete the module-level T = TypeVar("T") and the TypeVar import.
  • Give roundtrip its own type parameter, as def roundtrip[T](value: T, serializer: Serializer[T], parser: Parser[T]) -> T:. Keep its body, parser.load(serializer.dump(value)).
  • Leave ReprSerializer, IntParser, BoolParser, parse_all and solve as they are. Do not add cast or Any.

Each [T] declares a new parameter that belongs to that class or function alone, even though they share a letter.

What the driver returns

solve(ints, flags, raws) returns one list of strings, in three groups:

  1. Each integer, serialized with repr, parsed back with int, and shown with repr.
  2. Each boolean, through the same serializer and BoolParser, as "True" or "False".
  3. Each string in raws, parsed by IntParser and shown with repr. So "007" becomes "7", and "-0" becomes "0".

For ints=[-3, 0], flags=[False] and raws=["007", "-0"], it returns ["-3", "0", "False", "7", "0"].

Older code spells the same fix with two variables, such as TypeVar("T_contra", contravariant=True) and TypeVar("T_co", covariant=True).