Python Enhancement Proposals

PEP 663 – Standardizing Enum str(), repr(), and format() behaviors

PEP
663
Title
Standardizing Enum str(), repr(), and format() behaviors
Author
Ethan Furman <ethan at stoneleaf.us>
Discussions-To
python-dev@python.org
Status
Draft
Type
Informational
Created
30-Jun-2021
Python-Version
3.11
Post-History
20-Jul-2021, 02-Nov-2021
Resolution


Contents

Abstract

Update the repr(), str(), and format() of the various Enum types to better match their intended purpose. For example, IntEnum will have its str() change to match its format(), while a user-mixed int-enum will have its format() match its str(). In all cases, an enum’s str() and format() will be the same (unless the user overrides format()).

Add a global enum decorator which changes the str() and repr() (and format()) of the decorated enum to be a valid global reference: i.e. re.IGNORECASE instead of <RegexFlag.IGNORECASE: 2>.

Motivation

Having the str() of IntEnum and IntFlag not be the value causes bugs and extra work when replacing existing constants.

Having the str() and format() of an enum member be different can be confusing.

The addition of StrEnum with its requirement to have its str() be its value is inconsistent with other provided Enum’s str.

The iteration of Flag members, which directly affects their repr(), is inelegant at best, and buggy at worst.

Rationale

Enums are becoming more common in the standard library; being able to recognize enum members by their repr(), and having that repr() be easy to parse, is useful and can save time and effort in understanding and debugging code.

However, the enums with mixed-in data types (IntEnum, IntFlag, and the new StrEnum) need to be more backwards compatible with the constants they are replacing – specifically, str(replacement_enum_member) == str(original_constant) should be true (and the same for format()).

IntEnum, IntFlag, and StrEnum should be as close to a drop-in replacement of existing integer and string constants as is possible. Towards that goal, the str() output of each should be its inherent value; e.g. if Color is an IntEnum:

>>> Color.RED
<Color.RED: 1>
>>> str(Color.RED)
'1'
>>> format(Color.RED)
'1'

Note that format() already produces the correct output, only str() needs updating.

As much as possible, the str(), repr(), and format() of enum members should be standardized across the standard library. However, up to Python 3.10 several enums in the standard library have a custom str() and/or repr().

The repr() of Flag currently includes aliases, which it should not; fixing that will, of course, already change its repr() in certain cases.

Specification

There are three broad categories of enum usage:

  • simple: Enum or Flag a new enum class is created with no data type mixins
  • drop-in replacement: IntEnum, IntFlag, StrEnum a new enum class is created which also subclasses int or str and uses int.__str__ or str.__str__
  • user-mixed enums and flags the user creates their own integer-, float-, str-, whatever-enums instead of using enum.IntEnum, etc.

There are also two styles:

  • normal: the enumeration members remain in their classes and are accessed as classname.membername, and the class name shows in their repr() and str() (where appropriate)
  • global: the enumeration members are copied into their module’s global namespace, and their module name shows in their repr() and str() (where appropriate)

Some sample enums:

# module: tools.py

class Hue(Enum):  # or IntEnum
    LIGHT = -1
    NORMAL = 0
    DARK = +1

class Color(Flag):  # or IntFlag
    RED = 1
    GREEN = 2
    BLUE = 4

class Grey(int, Enum):  # or (int, Flag)
   BLACK = 0
   WHITE = 1

Using the above enumerations, the following two tables show the old and new output (blank cells indicate no change):

style category enum repr() enum str() enum format()
normal simple 3.10
new
user mixed 3.10 1
new Grey.WHITE
int drop-in 3.10 Hue.LIGHT
new -1
global simple 3.10 <Hue.LIGHT: -1> Hue.LIGHT Hue.LIGHT
new tools.LIGHT LIGHT LIGHT
user mixed 3.10 <Grey.WHITE: 1 Grey.WHITE Grey.WHITE
new tools.WHITE WHITE WHITE
int drop-in 3.10 <Hue.LIGHT: -1> Hue.LIGHT
new tools.LIGHT -1
style category flag repr() flag str() flag format()
normal simple 3.10 <Color.RED|GREEN: 3> Color.RED|GREEN Color.RED|GREEN
new <Color(3): RED|GREEN> Color.RED|Color.GREEN Color.RED|Color.GREEN
user mixed 3.10 <Grey.WHITE: 1> 1
new <Grey(1): WHITE> Grey.WHITE
int drop-in 3.10 <Color.RED|GREEN: 3> Color.RED|GREEN
new <Color(3): RED|GREEN> 3
global simple 3.10 <Color.RED|GREEN: 3> Color.RED|GREEN Color.RED|GREEN
new tools.RED|tools.GREEN RED|GREEN RED|GREEN
user mixed 3.10 <Grey.WHITE: 1> Grey.WHITE 1
new tools.WHITE WHITE WHITE
int drop-in 3.10 <Color.RED|GREEN: 3> Color.RED|GREEN
new tools.RED|tools.GREEN 3

These two tables show the final result:

style category enum repr() enum str() enum format()
normal simple <Hue.LIGHT: -1> Hue.LIGHT Hue.LIGHT
user mixed <Grey.WHITE: 1> Grey.WHITE Grey.WHITE
int drop-in <Hue.LIGHT: -1> -1 -1
global simple tools.LIGHT LIGHT LIGHT
user mixed tools.WHITE WHITE WHITE
int drop-in tools.LIGHT -1 -1
style category flag repr() flag str() flag format()
normal simple <Color(3): RED|GREEN> Color.RED|Color.GREEN Color.RED|Color.GREEN
user mixed <Grey(1): WHITE> Grey.WHITE Grey.WHITE
int drop-in <Color(3): RED|GREEN> 3 3
global simple tools.RED|tools.GREEN RED|GREEN RED|GREEN
user mixed tools.WHITE WHITE WHITE
int drop-in tools.RED|tools.GREEN 3 3

As can be seen, repr() is primarily affected by whether the members are global, while str() is affected by being global or by being a drop-in replacement, with the drop-in replacement status having a higher priority. Also, the basic repr() and str() have changed for flags as the old style was flawed.

Backwards Compatibility

Backwards compatibility of stringified objects is not guaranteed across major Python versions, and there will be backwards compatibility breaks where software uses the repr(), str(), and format() output of enums in tests, documentation, data structures, and/or code generation.

Normal usage of enum members will not change: re.ASCII can still be used as re.ASCII and will still compare equal to 256.

If the previous output needs to be maintained, for example to ensure compatibility between different Python versions, software projects will need to create their own enum base class with the appropriate methods overridden.

Note that by changing the str() of the drop-in category, we will actually prevent future breakage when IntEnum, et al, are used to replace existing constants.

Source: https://github.com/python/peps/blob/master/pep-0663.txt

Last modified: 2021-11-06 03:01:01 GMT