Proposal for two new typing constructs: `MemberNames[T]` and `MemberTypes[T]`

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.

1 Like

I like the idea of reducing friction between Enum and Literal, since in a lot of cases they can be used somewhat interchangeably, but I’m not sure what the utility of a TypedDict union is. Seems like in a lot of cases it would be equivalent to Any.

The “members” of an enum and a TypedDict are very different concepts, so I’m not sure it makes sense to use one construct covering both.

PEP 827, which was just created, proposes a Members[T] that works on TypedDicts and classes, but not enums (PEP 827 – Type Manipulation | peps.python.org).

2 Likes

Fair enough @hwelch / @Jelle , I was just worried about ending up with tons of almost-but-not-quite-the-same constructs rather than building on a more generalised idea.

How about we park the TypedDict thing and just focus on Enums - I was mostly focused on those anyway. Let’s say we spelled them as EnumNames[T] and EnumValues[T], and otherwise specified the same behaviour - no auto(), values have to be Literal-compatible types?

1 Like

I’m of the opinion that simply allowing a Literal generic on StrEnum is probably the easiest solution. The major issue with Enums and Literals is that they tend to get out of sync. Allowing the type checker to enforce that all members of a Literal are defined as members of an Enum would probably be enough for that:

Code = Literal['a', 'b', 'c']

class CodeEnum(StrEnum[Code]):
    A = 'a'
    B = 'b'
    # type error: 'c' member missing

Anything more complex than that would be better suited to one of the already existing proposals for KeyOf/TypeOf syntax.

Is it me or there is not even a discourse thread for this PEP 827?
I am reading it now - but it looks like the examples in motivation will need some rewrite (I, for one, can barely make sense of then, although, I’ve liked the opening paragraphs of the “motivation” section)

MemberTypes seems like a decent way to improve the ergonomics of Enums.

I don’t see a use case for MemberNames.

I can see a few utilities for it. Simple example:

from enum import Enum, auto
from typing_ideas import MemberNames

class Color(Enum):
    RED = auto()
    GREEN = auto()
    BLUE = auto()

def get_color(color: MemberNames[Color]) -> Color:
    return Color[color]

If you’re deserialising input and mapping values to enums, that pattern can be useful.

My personal use case is a little weirder, I want to reuse these names as valid attributes on another class:

class EventFilters(IntFlag):
    created = auto()
    deleted = auto()
    modified = auto()
    # (... bunch of other values ...)

class Event:
    def __init__(self, flags: EventFilters):
        self.flags = flags
    def __getattr__(self, name: MemberNames[EventFilters]) -> bool:
        return self.flags & EventFilters[name] == EventFilters[name] 

e = Event(EventFilters.created | EventFilters.modified)
e.created   # True
e.deleted   # False
e.modified  # True

i.e. allowing for a bit of dynamism in object creation where valid values can still be statically evaluated

There’s one linked at the top of the PEP

1 Like

(We were a little slow posting the discourse thread, so I think it wasn’t up yet when he posted, I think. It’s there now though!)

1 Like

Currently PEP 827 is semi-silent on enum types, except to say that in general, inferred types aren’t reflected in Members/Attrs. Enums are a special type system construct, though, and the type is embedded in the base class, so it wouldn’t be totally unreasonable to say that they ought to always have their members reported?

Then we’d be able to do:

type EnumNames[T] = Union[*[m.name for m in Iter[Attrs[T]]]]
type EnumTypes[T] = Union[*[m.init for m in Iter[Attrs[T]]]]

We’d need to think a little more about if this would/could accomplish what you want regarding compatibility between WARN and `"warn"` though

Using Attrs[T] here seems wrong because enum instances can themselves have attributes (e.g. Enum HOWTO — Python 3.14.3 documentation).

Ah, good catch. Then it’s something like:

type EnumNames[T] = Union[*[
    m.name for m in Iter[Attrs[T]]
    if not IsAssignable[m.init, Never]
]]
type EnumTypes[T] = Union[*[
    m.init for m in Iter[Attrs[T]]
    if not IsAssignable[m.init, Never]
]]

Do you think EnumNames / EnumTypes is worth pursuing here? PEP 827 (aside from not explicitly supporting Enums, whether or not it would enable an equivalent construct) seems pretty heavy and likely to require a lot of discussion/iteration.

Honestly, I’d love to see the basic Members or MemberNames / MemberTypes as its own standalone proposal purely on the basis of getting it across the line - I worry that such a heavyweight PEP might lead people to shelve any other discussions of what it proposes, so if it doesn’t get accepted or suffers delays, those discussions would be left in limbo.

Yes, if you’re interested in pursuing this, feel free!

I don’t think we need a MemberValues, I’d rather just have Union[*ErrorLevels] instead of yet another special construct. This is achievable by:

  1. Making enum classes eligible for typing.Unpack.
  2. Making typing.Union accept typing.Unpack (of tuple, TypeVarTuple, etc.) This has been requested many times already, and pyright internally already supports it.