Tagged unions
When you have a union of types, you can normally discriminate between each typein the union by using isinstance
checks. For example, if you had a variable x
oftype Union[int, str]
, you could write some code that runs only if x
is an intby doing if isinstance(x, int): …
.
However, it is not always possible or convenient to do this. For example, it is notpossible to use isinstance
to distinguish between two different TypedDicts sinceat runtime, your variable will simply be just a dict.
Instead, what you can do is label or tag your TypedDicts with a distinct Literaltype. Then, you can discriminate between each kind of TypedDict by checking the label:
- from typing import Literal, TypedDict, Union
- class NewJobEvent(TypedDict):
- tag: Literal["new-job"]
- job_name: str
- config_file_path: str
- class CancelJobEvent(TypedDict):
- tag: Literal["cancel-job"]
- job_id: int
- Event = Union[NewJobEvent, CancelJobEvent]
- def process_event(event: Event) -> None:
- # Since we made sure both TypedDicts have a key named 'tag', it's
- # safe to do 'event["tag"]'. This expression normally has the type
- # Literal["new-job", "cancel-job"], but the check below will narrow
- # the type to either Literal["new-job"] or Literal["cancel-job"].
- #
- # This in turns narrows the type of 'event' to either NewJobEvent
- # or CancelJobEvent.
- if event["tag"] == "new-job":
- print(event["job_name"])
- else:
- print(event["job_id"])
While this feature is mostly useful when working with TypedDicts, you can alsouse the same technique wih regular objects, tuples, or namedtuples.
Similarly, tags do not need to be specifically str Literals: they can be any typeyou can normally narrow within if
statements and the like. For example, youcould have your tags be int or Enum Literals or even regular classes you narrowusing isinstance()
:
- from typing import Generic, TypeVar, Union
- T = TypeVar('T')
- class Wrapper(Generic[T]):
- def __init__(self, inner: T) -> None:
- self.inner = inner
- def process(w: Union[Wrapper[int], Wrapper[str]]) -> None:
- # Doing `if isinstance(w, Wrapper[int])` does not work: isinstance requires
- # that the second argument always be an *erased* type, with no generics.
- # This is because generics are a typing-only concept and do not exist at
- # runtime in a way `isinstance` can always check.
- #
- # However, we can side-step this by checking the type of `w.inner` to
- # narrow `w` itself:
- if isinstance(w.inner, int):
- reveal_type(w) # Revealed type is 'Wrapper[int]'
- else:
- reveal_type(w) # Revealed type is 'Wrapper[str]'
This feature is sometimes called “sum types” or “discriminated union types”in other programming languages.