{
  "$type": "site.standard.document",
  "canonicalUrl": "https://rednafi.com/python/variance-of-generic-types/",
  "description": "Understand covariance, contravariance, and invariance in Python generics with practical examples of type relationships and subtyping rules.",
  "path": "/python/variance-of-generic-types/",
  "publishedAt": "2022-01-31T00:00:00.000Z",
  "site": "at://did:plc:fgtm2c26vfcj74rfmeggbyqj/site.standard.publication/3mnl6f7ob462z",
  "tags": [
    "Python",
    "Typing"
  ],
  "textContent": "I've always had a hard time explaining variance of generic types while working with type\nannotations in Python. This is an attempt to distill the things I've picked up on type\nvariance while going through PEP-483.\n\nA pinch of type theory\n\n> A generic type is a class or interface that is parameterized over types. Variance refers\n> to how subtyping between the generic types relates to subtyping between their parameters'\n> types.\n>\n> Throughout this text, the notation T2 <: T1 denotes T2 is a subtype of T1. A subtype\n> always lives in the pointy end.\n\nIf T2 <: T1, then a generic type constructor GenType will be:\n\n- Covariant, if GenType[T2] <: GenType[T1] for all such T1 and T2.\n- Contravariant, if GenType[T1] <: GenType[T2] for all such T1 and T2.\n- Invariant, if neither of the above is true.\n\nTo better understand this definition, let's make an analogy with ordinary functions. Assume\nthat we have:\n\nIf x1 < x2, then always cov(x1) < cov(x2), and contra(x2) < contra(x1), while nothing\ncould be said about inv. Replacing < with <:, and functions with generic type\nconstructors, we get examples of covariant, contravariant, and invariant\nbehavior.\n\nA few practical examples\n\nImmutable generic types are usually type covariant\n\nFor example:\n\n- Union behaves covariantly in all its arguments. That means: if T2 <: T1, then\n  Union[T2] <: Union[T1] for all such T1 and T2.\n\n- FrozenSet[T] is also covariant. Let's consider int and float in place of T. First,\n  int <: float. Second, a set of values of FrozenSet[int] is clearly a subset of values\n  of FrozenSet[float]. Therefore, FrozenSet[int] <: FrozenSet[float].\n\nMutable generic types are usually type invariant\n\nFor example:\n\n- list[T] is invariant. Although a set of values of list[int] is a subset of values of\n  list[float], only an int could be appended to a list[int]. Therefore, list[int] is\n  not a subtype of list[float].\n\nThe callable generic type is covariant in return type but contravariant in the arguments\n\n- Callable[[], int] <: Callable[[], float] .\n- If Manager <: Employee then Callable[[], Manager] <: Callable[[], Employee].\n\nHowever, for two callable types that differ only in the type of one argument, the subtype\nrelationship for the callable types goes in the opposite direction as for the argument\ntypes. Examples:\n\n- Callable[[float], None] <: Callable[[int], None], where int <: float.\n\n- Callable[[Employee], None] <: Callable[[Manager], None], where Manager <: Employee.\n\nI found this odd at first. However, this actually makes sense. If a function can calculate\nthe salary for a Manager, it should also be able to calculate the salary of an Employee.\n\nExamples\n\nCovariance\n\nHere, Dog <: Animal and notice how Mypy doesn't raise an error when a tuple of Dog\ninstance is passed into the action function that expects a sequence of Animal instances.\nHowever, if you make change the action function as follows:\n\nMypy will complain about this snippet since now, action expects a sequence of Dog\ninstance or a subtype of it. A sequence of Animal is not a subtype of a sequence of Dog.\nHence, the error.\n\nContravariance\n\nThe Callable generic type is covariant in return type. Here's how you can test it:\n\nHere, int <: float and the in the return type, you can see\nCallable[..., int] <: Callable[float] as Mypy is satisfied when either foo or bar is\npassed into the factory callable.\n\nOn the other hand, the Callable generic type is contravariant in the argument type.\nHere's how you can test it:\n\nHere, Mypy will complain in the case of factory(foo) as the factory function expects\nCallable[[float]], None] or its subtype. However, in the above case,\nCallable[[float]], None] <: Callable[[int], None] but not the other way around. That\ncauses the error.\n\nInvariance\n\nIn general, types defined with the TypeVar construct are invariant. You can mark them as\ncovariant or contravariant as well. However:\n\n> Remember that variance is a property of the generic types; not their parameter types.\n\nHere's how you can mark types as covariant, contravariant, or invariant:\n\nFurther reading\n\n- [PEP 483 - The theory of type hints]\n\n\n\n\n[pep 483 - the theory of type hints]:\n    https://www.python.org/dev/peps/pep-0483/#generic-types",
  "title": "Variance of generic types in Python"
}