How to suppress "index should be a string" type error on subclassed python enum

Viewed 152

The following example of accessing python Enum members by slice comes from its documentation (where Period happens to be an Enum):

list(Period)[:2]

When subclassing Enum, I thought it might make sense to permit a syntax that skips the wrapping in list (i.e. make the syntax for using my subclass more consistent with the syntax for accessing members of a list or tuple):

import enum

class MyEnumMeta(enum.EnumMeta):
  def __getitem__(self, indexOrSlice):
    if isinstance(indexOrSlice, (int, slice)):
      return list(self)[indexOrSlice]
    else:
      return enum.EnumMeta.__getitem__(self, indexOrSlice)

class MyEnum(enum.Enum, metaclass=MyEnumMeta): pass

class Period(MyEnum):
  A = "a"
  B = "b"
  C = "c"

print(Period["A"])
print(Period[1])    # type: ignore[misc]
print(Period[:2])   # type: ignore[misc]

Although this seems to execute fine, mypy is raising the following error unless I append # type: ignore[misc] to every line that attempts to use the "more condensed" syntax (which makes the syntax actually less condensed):

Enum index should be a string

(You can search for this in the mypy code, to see the relevant section)

I am uncomfortable with adding a statement that will suppress any legitimate [misc] errors that happen to occur in the same line. Furthermore, the fact that mypy went to the effort to raise this error (and that Enum doesn't permit the condensed syntax directly) makes me wonder whether there is some good reason why it should be impossible to access Enum members via integer or slice even in the way demonstrated in the Enum documentation.

If I shouldn't be doing what I am trying to do, can someone please explain why? Otherwise, can someone please suggest a better way to handle mypy? If mypy has made an error of failing to anticipate that the Enums it encounters might actually be subclasses of Enum that override __getitem__ as I propose, how do I report that error?

2 Answers

If I have an enum Foo, like so:

from enum import Enum

class Foo(Enum):
    BAR = 1
    BAZ = 2

Then, one of the many wonderful things about enums is that there are already multiple ways of accessing members of the enum. The following example is in the interactive console:

>>> Foo(1)
<Foo.BAR: 1>
>>> Foo['BAR']
<Foo.BAR: 1>
>>> Foo.BAR
<Foo.BAR: 1>
>>> Foo(Foo.BAR)
<Foo.BAR: 1>

As such, it may be unwise for you to override EnumMeta.__getitem__ in the way you're doing, as it may not be what others reading your code will expect. The inbuilt feature of enums is that you can access members by passing the member's name — a string — into the square brackets. Doing otherwise will violate the expectations of users of your code, and, as the MyPy error shows, it will also violate the expectations of a type checker. Explicitly converting the enum to a list outside of the class is somewhat different — the type checker, and any human readers of your code, are all well aware that lists can be indexed using integers or slices.

There is nothing wrong with your changes. Mypy has it's enum support hard-coded, though, so it won't recognize them.

Related