Variance of generic types in Python

Redowan Delowar January 31, 2022
Source
I've always had a hard time explaining variance of generic types while working with type annotations in Python. This is an attempt to distill the things I've picked up on type variance while going through PEP-483. A pinch of type theory > A generic type is a class or interface that is parameterized over types. Variance refers > to how subtyping between the generic types relates to subtyping between their parameters' > types. > > Throughout this text, the notation T2 <: T1 denotes T2 is a subtype of T1. A subtype > always lives in the pointy end. If T2 <: T1, then a generic type constructor GenType will be: - Covariant, if GenType[T2] <: GenType[T1] for all such T1 and T2. - Contravariant, if GenType[T1] <: GenType[T2] for all such T1 and T2. - Invariant, if neither of the above is true. To better understand this definition, let's make an analogy with ordinary functions. Assume that we have: If x1 < x2, then always cov(x1) < cov(x2), and contra(x2) < contra(x1), while nothing could be said about inv. Replacing < with <:, and functions with generic type constructors, we get examples of covariant, contravariant, and invariant behavior. A few practical examples Immutable generic types are usually type covariant For example: - Union behaves covariantly in all its arguments. That means: if T2 <: T1, then Union[T2] <: Union[T1] for all such T1 and T2. - FrozenSet[T] is also covariant. Let's consider int and float in place of T. First, int <: float. Second, a set of values of FrozenSet[int] is clearly a subset of values of FrozenSet[float]. Therefore, FrozenSet[int] <: FrozenSet[float]. Mutable generic types are usually type invariant For example: - list[T] is invariant. Although a set of values of list[int] is a subset of values of list[float], only an int could be appended to a list[int]. Therefore, list[int] is not a subtype of list[float]. The callable generic type is covariant in return type but contravariant in the arguments - Callable[[], int] <: Callable[[], float] . - If Manager <: Employee then Callable[[], Manager] <: Callable[[], Employee]. However, for two callable types that differ only in the type of one argument, the subtype relationship for the callable types goes in the opposite direction as for the argument types. Examples: - Callable[[float], None] <: Callable[[int], None], where int <: float. - Callable[[Employee], None] <: Callable[[Manager], None], where Manager <: Employee. I found this odd at first. However, this actually makes sense. If a function can calculate the salary for a Manager, it should also be able to calculate the salary of an Employee. Examples Covariance Here, Dog <: Animal and notice how Mypy doesn't raise an error when a tuple of Dog instance is passed into the action function that expects a sequence of Animal instances. However, if you make change the action function as follows: Mypy will complain about this snippet since now, action expects a sequence of Dog instance or a subtype of it. A sequence of Animal is not a subtype of a sequence of Dog. Hence, the error. Contravariance The Callable generic type is covariant in return type. Here's how you can test it: Here, int <: float and the in the return type, you can see Callable[..., int] <: Callable[float] as Mypy is satisfied when either foo or bar is passed into the factory callable. On the other hand, the Callable generic type is contravariant in the argument type. Here's how you can test it: Here, Mypy will complain in the case of factory(foo) as the factory function expects Callable[[float]], None] or its subtype. However, in the above case, Callable[[float]], None] <: Callable[[int], None] but not the other way around. That causes the error. Invariance In general, types defined with the TypeVar construct are invariant. You can mark them as covariant or contravariant as well. However: > Remember that variance is a property of the generic types; not their parameter types. Here's how you can mark types as covariant, contravariant, or invariant: Further reading - [PEP 483 - The theory of type hints] [pep 483 - the theory of type hints]: https://www.python.org/dev/peps/pep-0483/#generic-types

Discussion in the ATmosphere

Loading comments...