The subject of making enums and Literals more compatible has come up a few times (e.g. Amend PEP 586 to make `enum` values subtypes of `Literal` - #4 by Jelle or Proposal: LiteralEnum — runtime literals with static exhaustiveness - #20 by hwelch ). I’ve run into this limitation too, so I’d like to propose two new typing constructs: MemberNames[T] and MemberTypes[T].
The main motivation would be improving typing ergonomics for enums, especially StrEnum and IntEnum where users are looking for some kind of Literal[...] interoperability.
edit: removed non-Enum suggestions
I think the syntax here would be generalisable enough to apply more widely than just enums though, so to that end I’d like to include TypedDicts as well, since they contain relatively little magic or gotchas. (Contrast with something like dataclasses, which would require additional rules around InitVars, ClassVars, etc)
The basic semantics I had in mind:
For StrEnum and IntEnum, MemberTypes[T] or (or MemberValues[T]) would evaluate to a union of the explicitly spelled-out literal values of the enum:
class ErrorLevel(StrEnum):
WARN = "warn"
FATAL = "fatal"
def example(value: MemberTypes[ErrorLevel]) -> None: ...
Here, MemberTypes[ErrorLevel] would be be evaluated similarly to Literal["warn", "fatal"], with one crucial difference. Under the current typing spec, ErrorLevel.WARN isn’t assignable to Literal["warn"], even though it’s assignable to str, can’t be reassigned, and evaluates to "warn" at runtime. So this proposal would specify that for MemberTypes[ErrorLevel], both plain literals and enum members would be assignable:
f("warn") # ok
f(ErrorLevel.WARN) # also ok
MemberTypes would only be valid for StrEnum and IntEnum subtypes of enums, and only when all values are explicitly spelled out. Using auto() would be a type checking error, since it feels more in line with the strictness of Literal behaviour to require the values to be spelled out (and since it seems unreasonable to expect type checkers to re-implement the documented auto() logic themselves, or risk someone customising its behaviour). The result of invalid uses (using auto() or some other invalid value) would be treated as Never.
MemberNames[T] would evaluate to a union of the defined member names, including aliases (essentially the same as what ends up in __members__):
def example(name: MemberNames[ErrorLevel]) -> None: ... # evaluated as equivalent to `Literal["WARN", "FATAL"]`
(edit: removed non-Enum suggestions)
Removed the TypedDict stuff from this post
For a TypedDict, MemberNames[T] would evaluate to the union of declared keys, including inherited keys:
class Movie(TypedDict):
name: str
year: int
def example(key: MemberNames[Movie]) -> None: ... # evaluated as equivalent to `Literal["name", "year"]`
Closed/open, required/notrequired etc would be ignored here - It would simply be all the explicitly declared names.
MemberTypes[Movie] would evaluate to the union of the value types for each key:
def example(value: MemberTypes[Movie]) -> None: ... # equivalent to `str | int`
Required[T] and NotRequired[T] would be unwrapped to T. If PEP 728 typed extra_items are present, their type would be included in the resulting union.
There are other cool things that could be considered here (MemberType[TD, "key"] for example could allow for some pretty cool typed key/value access) but I think the above scope would be a reasonable balance between utility & scope.
What do you folks think? I haven’t had a crack at implementing anything (and I wouldn’t trust myself to do so reliably) but if there’s interest I’d be happy to do the PEP writing work and flesh it out.