We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Precise Type Hints with Literals and Generics step 22 of 26
List the Members of a Union at Runtime
Some tools read type hints while the program runs. A JSON serializer or a
settings loader, for example, may need to know that a field typed
int | str | None can hold an int, a str or None. Python gives you
two functions for this:
from typing import get_args, get_origin
hint = int | str | None
get_args(hint) # the members, (int, str, NoneType)
get_origin(hint) # the union class, which says what kind of type this is
Two things make this harder than it looks:
get_args(list[int])is(int,), butlist[int]is not a union. Checkget_originfirst.- A type alias made with
type, as in the recursive JSON exercise, stays an alias inside a union. Its__value__is the type it names:
type Inner = int | str
get_args(Inner | bytes) # (Inner, bytes)
get_args(Inner.__value__) # (int, str)
Inner is a TypeAliasType object. To see its members, expand it first.
What to write
flatten_union(alias: str) -> tuple[str, ...] takes a key of the supplied
REGISTRY and returns the names of the union's members:
- Expand the registry entry while it is a
TypeAliasType. If the result is not a union, return(). - Visit the members in the order
get_argsgives them. Expand each member that is an alias. If a member is itself a union, visit its members in turn. - Turn each member into a name. Use its
__name__when that is a string, andstr(member)otherwise. - Drop repeated names, keeping the first.
- Return the names as a tuple.
A value is a union when get_origin(value) in UNION_ORIGINS. The supplied
UNION_ORIGINS holds both union classes, typing.Union and
types.UnionType.
| Key | Result |
|---|---|
"pipe" or "union_form" |
("int", "str") |
"optional_int" |
("int", "NoneType") |
"alias_member" or "alias_itself" |
("int", "str", "bytes") |
"alias_overlap" |
("int", "str") |
"optional_alias" |
("int", "str", "NoneType") |
"plain_int" or "generic" |
() |
The registry holds no recursive aliases, so the expansion always ends.
Keep mypy checking your names
get_args returns tuple[Any, ...], and mypy does not check Any. So mypy
would accept a tuple that holds classes instead of names. Annotate your
helpers with object, for example expand(tp: object) -> object and
members(tp: object) -> list[object]. Read a name with
getattr(member, "__name__", None), and check isinstance(name, str) before
you return it.
Going further
On Python 3.12 and 3.13, int | str and Union[int, str] have different
origins, types.UnionType and typing.Union. Python 3.14 made
types.UnionType another name for typing.Union, so both give the same
origin. UNION_ORIGINS lists both, so the check works on every version.
Removing repeated names is this
exercise's rule, not a test of whether two types are equal. Two different
classes with the same name would merge.
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.