Formatted Python string uses neither repr nor str - what is happening?

Viewed 431

I have an enumeration ResourceType that inherits from both namedtuple and Enum, and I don't override __str__ or __repr__ anywhere. When I format an instance of that enum I unexpectedly get just the undecorated value, as opposed to either the repr() or the str(). How is this possible? What is being called?

Enum details (simplified):

from enum import Enum, auto
from collections import namedtuple

class ResourceType(namedtuple('ResourceType', 'value ext required'), Enum):
    RGB = auto(), '.png', True

Output:

>>> repr(ResourceType.RGB)
"<ResourceType.RGB: ResourceType(value=<enum.auto object at 0x7f44b7d48d30>, ext='.png', required=True)>"

>>> str(ResourceType.RGB)
'ResourceType.RGB'

>>> f"{ResourceType.RGB}"
"ResourceType(value=<enum.auto object at 0x7f44b7d48d30>, ext='.png', required=True)"

The last value is neither the repr() nor the str(), so even if namedtuple is providing that string, why does it not also provide the str/repr?

3 Answers

When you insert an object into an f-string in this way it calls the __format__ method.

from enum import Enum, auto
from collections import namedtuple

class ResourceType(namedtuple('ResourceType', 'value ext required'), Enum):
    RGB = auto(), '.png', True

    def __repr__(self):
        return "REPR"

    def __str__(self):
        return "STR"

    def __format__(self, format_spec):
        return "FORMAT"

print(repr(ResourceType.RGB))
print(str(ResourceType.RGB))
print(f"{ResourceType.RGB}")

gives output of

REPR
STR
FORMAT

Now that Kemp and Daweo have pointed out that the magic is happening via __format__, I was able to dig a little deeper, and indeed inside the Enum class we find:

def __format__(self, format_spec):
    # mixed-in Enums should use the mixed-in type's __format__, otherwise
    # we can get strange results with the Enum name showing up instead of
    # the value

    # pure Enum branch
    if self._member_type_ is object:
        cls = str
        val = str(self)
    # mix-in branch
    else:
        cls = self._member_type_
        val = self._value_
    return cls.__format__(val, format_spec)

ResourceType is a mixed-in enum due to the inheritance of namedtuple as well, and so for the __format__ case, the call is redirected to the 'ResourceType' namedtuple with just the value of the enum instance, which is stored in _value_ by convention/implementation.

In my case I wanted the ResourceType enum to appear externally like an enum as much as possible, despite also being a namedtuple, so I have now changed it to:

class ResourceType(namedtuple('ResourceType', 'value ext required'), Enum):
    RGB = auto(), '.png', True
    def __format__(self, format_spec):
        return str.__format__(str(self), format_spec)

This is exactly equivalent to just forcing the 'pure Enum branch' in the Enum.__format__ implementation.

What is being called?

It is Enum.__format__ whose doc-string in enum.py states that it

Returns format using actual value type unless __str__ has been overridden.

Related