# Python Invariance: list vs. Sequence ## Key Terms 1. **Covariant**: If A is a subtype of B, then Container[A] is a subtype of Container[B]. 2. **Contravariant**: If A is a subtype of B, then Container[B] is a subtype of Container[A]. 3. **Invariant**: Neither covariant nor contravariant. Container[A] and Container[B] are not related, even if A and B are. ## The Case of list In Python, `list` is invariant. This means a `list[Dog]` is not considered a subtype of `list[Animal]`, even though `Dog` is a subtype of `Animal`. ### Why list is Invariant To understand why `list` is invariant, let's consider what could happen if it were covariant: ```python class Animal: def make_sound(self): pass class Dog(Animal): def make_sound(self): print("Woof!") def bark(self): self.make_sound() class Cat(Animal): def make_sound(self): print("Meow!") def add_cat(animals: list[Animal]): animals.append(Cat()) # If list were covariant (which it's not in Python): dogs: list[Dog] = [Dog(), Dog()] add_cat(dogs) # This would be allowed, but shouldn't be! # Later in the code: for dog in dogs: dog.bark() # Oops! One of these will fail because it's actually a Cat ``` If `list` were covariant: 1. We could pass a `list[Dog]` to a function expecting `list[Animal]`. 2. That function could add a `Cat` to the list. 3. Now our `dogs` list contains a `Cat`, which breaks our assumption. 4. This could cause unexpected behavior or errors later in the code. By making `list` invariant, Python prevents this kind of mistake. It forces you to be explicit about what types of objects can be in your list, making your code safer and less prone to runtime errors. ## The Case of Sequence Unlike `list`, `Sequence` is covariant in Python. This means you can use a `Sequence[Dog]` where a `Sequence[Animal]` is expected. ### Why Sequence can be Covariant `Sequence` is a read-only protocol. This means you can't add or remove items from a `Sequence`. This key difference makes it safe for `Sequence` to be covariant: ```python from typing import Sequence def process_animals(animals: Sequence[Animal]): for animal in animals: animal.make_sound() # We can't add to the sequence here dogs: list[Dog] = [Dog(), Dog()] process_animals(dogs) # This works fine ``` Key points: 1. You can pass a `list` to a function expecting a `Sequence`. This is because `list` implements the `Sequence` protocol. 2. The function receiving a `Sequence` can't add to it. `Sequence` is a read-only protocol. 3. You can't write an `add_cat` function for `Sequence`: ```python def add_cat(animals: Sequence[Animal]): animals.append(Cat()) # This would cause an error ``` This function would fail because `Sequence` doesn't have an `append` method. ## Conclusion The invariance of `list` and the covariance of `Sequence` in Python serve different purposes: - `list` being invariant prevents type errors that could occur from modifying the list. - `Sequence` being covariant allows for more flexible use of read-only sequences, without the risk of modification-related type errors. Understanding these differences helps in writing more type-safe and flexible Python code.