PEP: 663 Title: Standardizing Enum str(), repr(), and format() behaviors
Version: $Revision$ Last-Modified: $Date$ Author: Ethan Furman
<ethan@stoneleaf.us> Discussions-To: python-dev@python.org Status:
Rejected Type: Informational Content-Type: text/x-rst Created:
30-Jun-2021 Python-Version: 3.11 Post-History: 20-Jul-2021, 02-Nov-2021
Resolution:
https://mail.python.org/archives/list/python-dev@python.org/message/RN3WCRZSTQR55DOHJTZ2KIO6CZPJPCU7/

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        3.10                    Hue.LIGHT    
           drop-in                                         

                      new                     -1           

  global   simple     3.10     <Hue.LIGHT:    Hue.LIGHT    Hue.LIGHT
                               -1>                         

                      new      tools.LIGHT    LIGHT        LIGHT

           user mixed 3.10     <Grey.WHITE: 1 Grey.WHITE   Grey.WHITE

                      new      tools.WHITE    WHITE        WHITE

           int        3.10     <Hue.LIGHT:    Hue.LIGHT    
           drop-in             -1>                         

                      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        3.10    <Color.RED|GREEN: 3>    Color.RED|GREEN         
           drop-in                                                            

                      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        3.10    <Color.RED|GREEN: 3>    Color.RED|GREEN         
           drop-in                                                            

                      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        <Color(3): RED|GREEN>   3                       3
           drop-in                                                    

  global   simple     tools.RED|tools.GREEN   RED|GREEN               RED|GREEN

           user mixed tools.WHITE             WHITE                   WHITE

           int        tools.RED|tools.GREEN   3                       3
           drop-in                                                    
  -------- ---------- ----------------------- ----------------------- -----------------------

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.