Search code examples
pythoninheritanceenumsmethod-resolution-order

How does python3.11's StrEnum's MRO work differently for __str__ and __repr__?


Python3.11 introduced StrEnum and IntEnum which inherit str or int respectively, and also inherit ReprEnum, which in turn inherits Enum.

ReprEnum's implementation is actually empty.

>>> print(inspect.getsource(ReprEnum))
class ReprEnum(Enum):
    """
    Only changes the repr(), leaving str() and format() to the mixed-in type.
    """

If I create a StrEnum and check the MRO, I can see that str comes first.

class Strings(StrEnum):
    A = "a"
>>> Strings.__mro__
(<enum 'Strings'>, <enum 'StrEnum'>, <class 'str'>, <enum 'ReprEnum'>, <enum 'Enum'>, <class 'object'>)

Both str and Enum define a __str__ and a __repr__

>>> str.__repr__
<slot wrapper '__repr__' of 'str' objects>
>>> str.__str__
<slot wrapper '__str__' of 'str' objects>
>>> Enum.__repr__
<function Enum.__repr__ at 0x7ffff69f72e0>
>>> Enum.__str__
<function Enum.__str__ at 0x7ffff69f7380>

How then does __repr__ get inherited from Enum and __str__ get inherited from str?


Solution

  • The __repr__ method comes the normal way, inherited from Enum (via StrEnum)

    >>> Strings.__repr__ is StrEnum.__repr__ is Enum.__repr__
    True
    

    For the __str__ method, the metaclass EnumType checks for the presence of ReprEnum and "hoists up" the str and format handling of the mixed-in data type into the class namespace at class definition time here:

    class EnumType(type):
    
        ...
    
        def __new__(metacls, cls, bases, classdict, *, boundary=None, _simple=False, **kwds):
    
            ...
    
            # Also, special handling for ReprEnum
            if ReprEnum is not None and ReprEnum in bases:
                if member_type is object:
                    raise TypeError(
                            'ReprEnum subclasses must be mixed with a data type (i.e.'
                            ' int, str, float, etc.)'
                            )
                if '__format__' not in classdict:
                    enum_class.__format__ = member_type.__format__
                    classdict['__format__'] = enum_class.__format__
                if '__str__' not in classdict:
                    method = member_type.__str__
                    if method is object.__str__:
                        # if member_type does not define __str__, object.__str__ will use
                        # its __repr__ instead, so we'll also use its __repr__
                        method = member_type.__repr__
                    enum_class.__str__ = method
                    classdict['__str__'] = enum_class.__str__
    
            ...
    

    Now that a Strings.__str__ method may be found directly in the class namespace, the MRO needn't be traversed.