Suppose I have a function that takes a string parameter that’s supposed to be one of a limited set of strings, and I want a type checker to catch it if the caller passes something else. This is what typing.Literal is for…
from typing import Literal
ASPECT_TABLE_TYPE = Literal["aspect", "aspect2", "boresight", "metadata"]
def get_aspect_table(table: ASPECT_TABLE_TYPE) -> ...:
# retrieve the requested table
But I also need a runtime check, because maybe the caller of my library didn’t bother running a type checker. So I’d like to write something like
def get_aspect_table(table: ASPECT_TABLE_TYPE) -> ...:
if table not in ASPECT_TABLE_TYPE:
raise ValueError(f"unrecognized table {table}")
# retrieve the requested table
And here’s the actual question: I can’t find any way of writing if table not in ASPECT_TABLE_TYPE that both the main Python interpreter (>=3.10, if it matters) and type checkers are happy with.
The typing documentation does not state any official way to do this.
if table not in ASPECT_TABLE_TYPE provokes TypeError: typing.Literal[...] is not a generic class from the main interpreter.
if table not in ASPECT_TABLE_TYPE.__args__ works in the main interpreter but provokes error: "<typing special form>" has no attribute "__args__" [attr-defined] from mypy.
If I try to pull the arguments of Literal out to a separate variable,
You could use from typing_inspect import get_args,
or do what this does inside which is to ask first hasattr(tp, '__args__') before res = tp.__args__,
or getattr(ASPECT_TABLE_TYPE, '__args__')
IIRC the inspect.get_args approach doesn’t work for unions of literals either. It gets the arguments to the union which is the literal types, not the literal values.
I answered a similar question on stack overflow which I’ve used for PEP695 type aliases but I think might work for old aliases too:
from collections.abc import Sequence, Iterator
from typing import Literal, get_args, TypeAliasType, cast
def get_literal_vals(alias: TypeAliasType) -> frozenset:
def val(alias: TypeAliasType):
return alias.__value__
def args(alias: TypeAliasType):
return get_args(val(alias))
def resolve[T](alias: TypeAliasType | tuple[T, ...] | T) -> Iterator[T]:
if isinstance(alias, TypeAliasType):
for val in resolve(args(alias)):
yield from resolve(val)
return
if isinstance(alias, tuple):
t_seq = cast(Sequence[T], alias)
for element in t_seq:
yield from resolve(element)
return
yield alias
return frozenset(resolve(alias))
type Doubles = Literal["ab", "de", "gh"]
type Triples = Literal["abc", "def", "ghi"]
type DT = Doubles | Triples
dt_set: frozenset[DT] = get_literal_vals(DT)