Type Hinting Generic Enum Factories

The other day I was putting together some code for a silly personal project, I was setting up a service to allow users to query magic the gathering cards using the MTG public API. Well, while setting up the prompts for the user, I realized I wanted to have a generic way to provide the user a set of option given an enum, and return an instance of the enum chosen by the user. Sounds simple, but generalizing this without making mypy mad turned out to require some trickery.

I have a function that given an enum, will display its members as options to the user:

  from enum import Enum, auto

  def menu_from_enums(e: type[Enum]) -> Enum:
    menu_options = e._member_names_
    menu_options_w_buttons = [f"{i}: {v}" for i, v in enumerate(menu_options)]
    print(menu_options_w_buttons)  # type: ignore
    value: int = int(input("Insert on of the options provided \n>"))
    return e(value)

I can then have an enum of MTG card types like this, where I carefully put the least meaningful option as first:

  class MagicCardType(Enum):
    NONCOLOR = auto()
    SWAMP = auto()
    PLAINS = auto()
    FOREST = auto()
    ISLAND = auto()
    MOUNTAIN = auto()

and then I can do something like this to get the enum chosen by the user:

  mtg_type: MagicCardType = menu_from_enums(MagicCardType)

However, mypy will complain here, because our generilized funcion returns an instance of an Enum, so although we can use this function with any Enum, we cannot get the type hinting desired. Solution? Well, it requires boilerplate code, as many other things in Python, but I found out I can do it by making a dedicated wrapper function per Enum (kind of defeats the purpose of the generic menu function to begin with, but it makes the static type checker happy, so…):

  def to_mtg(e: Enum) -> MagicCardType:
    a: MagicCardType = MagicCardType(1)
    if isinstance(e, MagicCardType):
        a: MagicCardType = e
    return a

With this option, we enforce the specific Enum class we expect, if we pass an Enum which is not the expected one, we will return the 0 value (1 in case of python enums…) of the expected Enum class:

  mtg_type: MagicCardType = menu_from_enums(MagicCardType)

Now mypy is happy! However… We get a little complain on the double assignment in the function. We can # type: ignore that, or, we can opt to raise an exception instead of returning a 0 (1 lol) value:

  def to_mtg(e: Enum) -> MagicCardType:
    if isinstance(e, MagicCardType):
        a: MagicCardType = e
    else:
      raise TypeError("Wrong Enum Passed! Expected MagicCardType, got %s" % type(e))
    return a

And with this, mypy is forever happy.