6

In my Enum, i have defined a classmethod for coercing a given value to an Enum member. The given value may already be an instance of the Enum, or it may be a string holding an Enum value. In order to decide whether it needs conversion, i check if the argument is an instance of the class, and only pass it on to int() if it is not. At which point – according to the type hints for the argument 'item' – it must be a string.

The class look like this:

T = TypeVar('T', bound='MyEnum')

class MyEnum(Enum):
    A = 0
    B = 1

    @classmethod
    def coerce(cls: Type[T], item: Union[int, T]) -> T:
        return item if isinstance(item, cls) else cls(int(item))

mypy fails with:

error: Argument 1 to "int" has incompatible type "Union[str, T]"; expected "Union[str, bytes, SupportsInt, SupportsIndex, _SupportsTrunc]"

Why?

1 Answer 1

6

It's because int cannot be constructed from Enum. From documentation of int(x)

If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in radix base

So to make it work you can make your Enum inherit from str

class MyEnum(str, Enum):
    A = 0
    B = 1

Or use IntEnum

class MyEnum(IntEnum):
    A = 0
    B = 1

Edit: The question why isinstance doesn't narrow T type enough still left. Couldn't find any prove of my theory, but my (hopefully logical) explanation is - T is bounded to MyEnum so it's MyEnum and any subclass of it. If we have subclass scenario, isinstance(item, MyEnumSubclass) won't exclude case when item is just MyEnum class. If we use MyEnum directly in typing, problem will disappear.

    @classmethod
    def coerce(cls: t.Type["MyEnum"], item: t.Union[int, T]) -> "MyEnum":
        return item if isinstance(item, cls) else cls(int(item))
Sign up to request clarification or add additional context in comments.

5 Comments

Wouldn't if isinstance(item, cls) serve as a type guard to avoid constructing an int from an instance of MyEnum?
@kosciej16 no, it does in fact work, if you call with a stringified version of one the intsavailable. MyEnum.coerce('0') -> <MyEnum.A: 0>
Deriving from IntEnum however, does pacify mypy about it and seems an elegant solution. still don't completely understand why the "guard and cast" approach is so upsetting.
After some trials with mypy stuff and reveal_type I think I found an explanation.
Another thing that works along the same line of reasoning is a method signature of def coerce(cls: Type['MyEnum'], item: Union[str, 'MyEnum']) -> 'MyEnum': and a return value of return item if isinstance(item, MyEnum) else cls(int(item))

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.