TypeVar that can be any enum

I am trying to write a simple function that takes a string and an enum type and tries to create the matching enum instance and give a nice error message if not.

However, i just tried out the new rust type checkers and noticed that zuban gives the following error

error: Enum() expects a string literal as the first argument [misc]

A minimal reproducer of that issue is.

from typing import TypeVar, reveal_type
from enum import Enum

_E = TypeVar("_E", bound=Enum)

def construct_from_string(key: str, enum_type: type[_E]) -> _E:
    return enum_type(key.lower())

Originally i thought this was a false positive as not other type checker (pyright, mypy, pyrefly) report it. But it actually is correct.

While my use is

class Color(Enum):
    RED = "red"
    BLUE = "blue"


color  = construct_from_string("RED", Color)

Where this works perfectly fine, the type hint also allows this:

base_enum = construct_from_string("anything", Enum)

Which crashes at runtime with

TypeError: <enum ‘Enum’> has no members; specify names=() if you meant to create a new, empty, enum

Is there any way to type hint this correctly? (And should i create issues for the other type checkers for them allowing the current version)

I agree that type-checkers should allow Enum to be used as TypeVar bound.

In this particular case, you could work around it using a structural typing approach:

from typing import Callable, TypeVar, reveal_type
from enum import Enum

_T = TypeVar("_T")


def construct_from_string(key: str, enum_type: Callable[[str], _T]) -> _T:
    return enum_type(key.lower())

class Color(Enum):
    RED = "red"
    BLUE = "blue"


reveal_type(construct_from_string("red", Color))  # Color

mypy playground
basedpyright playground

I actually had that (although zuban then complained about that when i tried passing in the enum type…) but unfortunatley it doesnt work regardless because i need to iterate over the enum to get the options for my error message ;(

Well in that case you could use something like

from typing import Iterator, Protocol

class _EnumLike[T](Protocol):
    def __call__(self,  key: str, /) -> T: ...
    def __iter__(self, /) -> Iterator[T]: ...

def construct_from_string[T](key: str, enum_type: _EnumLike[T]) -> T:
    return enum_type(key.lower())

I think what you want here is to constrain the type of key to the enum values defined in enum_type. For that I think you’d need the proposed PEP 827 (Type Manipulation) which would allow something like

def construct[E](key: Attrs[E], enum: Type[E]) -> E:
    return E(key)

Though I don’t know how to type the .lower() part. But maybe that’s okay – if the caller gets the case wrong they’ll get a type error.

EDIT:* I chatted with Claude about this and Attrs does not produce a union. You could try KeyOf but it would require annotating the enum, which is uncommon. So, no, unless we add new rules for Enum subclasses. Also, I should have used [E: Enum].

Thanks! Got too tunneled on nominal typing i think.

zuban is still complaining, where ForceOption is a simple StrEnum

src/pymend/pymendapp.py:458: error: Argument 3 to "_validate_enum_config" has incompatible type "type[ForceOption]"; expected "_EnumLike[ForceOption]"  [arg-type]
src/pymend/pymendapp.py:458: note: Following member(s) of "ForceOption" have conflicts:
src/pymend/pymendapp.py:458: note:     Expected:
src/pymend/pymendapp.py:458: note:         def __call__(str, /) -> _E_co
src/pymend/pymendapp.py:458: note:     Got:
src/pymend/pymendapp.py:458: note:         @overload
src/pymend/pymendapp.py:458: note:         def __call__(value: Any, names: None = ...) -> ForceOption
src/pymend/pymendapp.py:458: note:         @overload
src/pymend/pymendapp.py:458: note:         def __call__(value: Any, *values: Any) -> ForceOption

class ForceOption(StrEnum):

    FORCE = "force"
    UNFORCE = "unforce"
    NOFORCE = "noforce"

All other type checkers still accept it.

@davidhalter, any idea what’s going on here?

This is probably just a small problem with Enums that hasn’t come up before. Enums are quite special and structural typing might be a bit off in some ways. @JanEricNitschke just open issues on GitHub and I hope to eventually have time to look at it.