PEP 663 – Standardizing Enum str(), repr(), and format() behaviors
- Author:
- Ethan Furman <ethan at stoneleaf.us>
- Discussions-To:
- Python-Dev list
- Status:
- Rejected
- Type:
- Informational
- Created:
- 30-Jun-2021
- Python-Version:
- 3.11
- Post-History:
- 20-Jul-2021, 02-Nov-2021
- Resolution:
- Python-Dev message
Table of 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
orFlag
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 subclassesint
orstr
and usesint.__str__
orstr.__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 theirrepr()
andstr()
(where appropriate) - global: the enumeration members are copied into their module’s global
namespace, and their module name shows in their
repr()
andstr()
(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.
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.
Source: https://github.com/python/peps/blob/main/peps/pep-0663.rst
Last modified: 2023-09-09 17:39:29 GMT