We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Precise Type Hints with Literals and Generics step 16 of 26
Control Variance in Your Own Generic Classes
Build three small generic classes that pass dogs around. A Producer hands
out values, a Consumer takes them in, and a Cell holds one value that you
can read and replace. Which one can stand in for which depends on what each
class lets callers do with T.
The exercise "Why a list[Dog] Is Not a list[Animal]" showed that Sequence is
covariant and list is invariant. When you write class Producer[T]:, mypy
works out the variance of your own class. It looks at how the public methods
and attributes use T. Shape each class so that mypy infers this:
| Class | Relationship | Why it is safe |
|---|---|---|
Producer |
Producer[Dog] can be used as Producer[Animal] |
Every Dog it hands out is an Animal. |
Consumer |
Consumer[Animal] can be used as Consumer[Dog] |
A consumer that accepts any Animal can accept Dogs. |
Cell |
Neither substitution is allowed | Reading and writing need the same element type. |
Producer is covariant, and Cell is invariant. Consumer is
contravariant, which goes the other way. Something that accepts any
Animal can stand in for something that accepts a Dog. mypy checks these
relationships. Python ignores them at runtime.
What to write
Producer[T]:
- The constructor takes a
Sequence[T]. Storetuple(items)in a private attribute,_items. - Expose it through a read-only
itemsproperty that returnstuple[T, ...], with no setter. - Later changes to the caller's sequence must not change the producer's items. Keep the same item objects, without deep copies.
Consumer[T]:
- It starts empty, and each consumer has its own storage.
put(self, item: T) -> Nonestores each item in arrival order, duplicates included.log(self) -> list[str]returns a new list ofstr(item)for each stored item, computed when you calllog.- Store the items in a private
list[object]. Then the class acceptsTbut never returns it.
Cell[T]:
- The constructor takes a value and stores it in a public attribute,
value: T, that callers can read and assign.
pipe:
def pipe[T](producer: Producer[T], consumer: Consumer[T]) -> None:
...
- Call
consumer.putonce for each item ofproducer.items, in order. - Leave the producer unchanged, and keep any items already in the consumer.
- An empty producer makes no calls. Piping the same producer twice puts its items twice.
Why the starter's Producer is invariant
The starter keeps a public list[T] in Producer.items. If mypy let a
Producer[Dog] stand in for a Producer[Animal], a caller could append a
plain Animal to that list of dogs. So mypy infers invariance. A read-only
property that returns a tuple leaves callers no way to add items.
mypy treats an attribute whose name starts with an underscore as read-only
when it infers variance. It still counts the attribute's type, so
_items: list[T] would make Producer invariant again, and a private
list[T] would do the same to Consumer. Python still lets any code reach
_items at runtime.
mypy also leaves __init__ out, so a constructor that takes T does not stop
Producer from being covariant. A read-only property that returns list[T]
still makes the class invariant, because callers can change the list it
returns.
Keep the solve driver
The starter defines Animal and its subclass Dog. str(Dog("rex")) is
"Dog(rex)".
solve(names) makes one Dog per name, in order and with duplicates, and
pipes them into a consumer. It then creates a Cell[Animal] that holds
Animal("origin"), and sets its value to Dog("swapped"). It returns the
consumer's log, the number of producer items, and the string of the cell's
value. Empty and non-ASCII names are valid. Leave the input list unchanged.
solve(["rex", "fido"]) # (["Dog(rex)", "Dog(fido)"], 2, "Dog(swapped)")
solve([]) # ([], 0, "Dog(swapped)")
solve(["", "ré", "ré"]) # (["Dog()", "Dog(ré)", "Dog(ré)"], 3, "Dog(swapped)")
mypy checks your classes through these annotated assignments in the driver:
dog_producer: Producer[Dog] = Producer(dogs)
producer: Producer[Animal] = dog_producer
sink: Consumer[Dog] = Consumer[Animal]()
Keep them as they are, with no cast and no # type: ignore. The separate
dog_producer variable matters. Suppose the driver wrote
producer: Producer[Animal] = Producer(dogs) instead. mypy would infer
Producer[Animal] from the annotation, so that line passes even when
Producer is invariant.
To see that Cell is invariant, check in a scratch file that mypy rejects
each of these lines. Do not put them in your submission, because they fail
mypy on purpose:
a: Cell[Animal] = Cell[Dog](Dog("d"))
b: Cell[Dog] = Cell[Animal](Animal("a"))
See mypy's guide to variance for more examples.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Python reference solution
Sign in to track your attempts toward unlocking the reference solution.