skip to content

What does enum.ReprEnum change about how enum.IntEnum members print?

level: seniorimportance: should knowfreq 28%

answer

  1. The issue is text, not equality
  2. repr stays informative, str does not
  3. The mixed-in type supplies `__str__`
  4. Changed in Python 3.11
  5. Use `.name` for a stable label

basics

~10 s

enum.ReprEnum keeps Enum's __repr__ while letting the mixed-in data type supply __str__ and __format__. Because enum.IntEnum and enum.StrEnum derive from it, str(Priority.HIGH) has been '3' since Python 3.11 - it was 'Priority.HIGH' before.

solid answer

~40 s

`enum.ReprEnum`, added in **3.11**, is the base that says: preserve Enum's diagnostic `__repr__`, but hand `__str__` and `__format__` to the mixed-in data type. `enum.IntEnum`, `enum.StrEnum` and `enum.IntFlag` all derive from it, so on 3.14 `str(Priority.HIGH)` and `f"{Priority.HIGH}"` are both `'3'`, `f"{Priority.HIGH:03d}"` is `'003'`, and `repr()` is still `<Priority.HIGH: 3>`. Before 3.11 `str()` gave `'Priority.HIGH'` while formatting already gave `'3'` - the change made them consistent. The contrast that catches people is a hand-rolled `class Legacy(int, Enum)`: identical equality and arithmetic, but it is not a `ReprEnum`, so it still prints `'Legacy.HIGH'`. Practically: never let a human-readable label come from `str()` of a member - ask for `.name`.

code

python · 14 lines
python
from enum import Enum, IntEnum, ReprEnum

class Priority(IntEnum):
    HIGH = 3

class Legacy(int, Enum):
    HIGH = 3

class Ratio(float, ReprEnum):
    HALF = 0.5

print(str(Priority.HIGH), f"{Priority.HIGH}", repr(Priority.HIGH))
print(str(Legacy.HIGH), f"{Legacy.HIGH}", repr(Legacy.HIGH))
print(str(Ratio.HALF), repr(Ratio.HALF), Priority.HIGH.name)

go deeper

for a junior

Recall that printing an enum.IntEnum member shows the bare number while printing a plain enum.Enum member shows Class.NAME, and that .name is how you ask for the name on purpose.

for a middle

Explain what enum.ReprEnum does - Enum's __repr__ is kept, __str__ and __format__ come from the mixed-in type - and name 3.11 as the release that made str() of an IntEnum consistent with its formatting.

for a senior

Treat it as an upgrade hazard: know that the flip is silent, that a hand-rolled (int, Enum) class behaves differently from IntEnum here, and be able to describe the audit of format strings and log arguments you would run before the bump.

for a principal

Own the standard that any string leaving the system - log field, metric label, cache key, URL segment - is produced deliberately rather than by interpolating an object, so an interpreter upgrade can never change a wire-visible string.

### The problem ReprEnum was created to solve Two audiences read an enum member as text. A human reading a traceback or a debugger wants `<Priority.HIGH: 3>` - the class, the name, the value. Code that is formatting a value for a protocol wants `3`. Before **Python 3.11** the mixed-in kinds served neither audience consistently: `str(Priority.HIGH)` gave `"Priority.HIGH"` while `format(Priority.HIGH)` - and therefore an f-string - already gave `"3"`. The same member rendered two different ways depending on which formatting route you took. `enum.ReprEnum`, added in 3.11, resolves that. It is a base class that means: **keep Enum's `__repr__`, but let the mixed-in data type supply `__str__` and `__format__`.** The name is counter-intuitive - it does not change `__repr__`, it *preserves* it while surrendering everything else to the data type. ### What that produces on 3.14 `enum.IntEnum`, `enum.StrEnum` and `enum.IntFlag` all derive from `ReprEnum`, so for `class Priority(IntEnum): HIGH = 3`: ``` str(Priority.HIGH) -> '3' f"{Priority.HIGH}" -> '3' f"{Priority.HIGH:03d}" -> '003' # int.__format__ honours the int spec "%s" % Priority.HIGH -> '3' repr(Priority.HIGH) -> '<Priority.HIGH: 3>' Priority.HIGH.name -> 'HIGH' ``` Contrast the hand-rolled mixin `class Legacy(int, Enum): HIGH = 3`, which does **not** inherit from `ReprEnum`: its equality and arithmetic are identical to `IntEnum`'s, but `str(Legacy.HIGH)` and `f"{Legacy.HIGH}"` are both `"Legacy.HIGH"`. Two classes with the same values and the same comparison semantics, printing completely differently. That is the distinction the question is really about, and it is why "just use `(int, Enum)`" is not interchangeable advice. You can use the base directly for other data types: `class Ratio(float, ReprEnum): HALF = 0.5` gives `str(Ratio.HALF) == "0.5"` with `repr()` still `<Ratio.HALF: 0.5>`. `ReprEnum` requires a data-type mixin; on its own it has nothing to borrow `__str__` from. For completeness, a plain `enum.Enum` is unaffected by any of this: `str(Color.RED)` and `f"{Color.RED}"` are both `"Color.RED"` on 3.14, and its `repr()` is `<Color.RED: 1>`. ### Why this is a production question, not trivia The rendering is invisible until something interpolates a member into a string, and then it is everywhere: log messages built with `%s` or an f-string, metric and span labels, cache keys, URL path segments, filenames, error text shown to a user. A service that upgrades from 3.10 to 3.11 sees every one of those flip from `Priority.HIGH` to `3` at once - no exception, no test failure unless a test asserts on the text, just a different string in production. If any of those strings is parsed downstream, keyed on, or grouped by a dashboard, the change is a real incident: the dashboard's series splits, the cache misses on every entry, the alert rule matching `priority=HIGH` stops matching. Landing that mid-way through a three-week release train, with the interpreter bump bundled among other changes, makes it genuinely hard to attribute. ### The habit that makes it a non-issue Never let the human-readable form of an enum be an accident of `str()`. Where a label is wanted, ask for it: `member.name` for the identifier, an explicit mapping or a `label` property for user-facing text, and `member.value` where the raw datum belongs. Reserve `str()` and bare f-string interpolation for cases where you actively want the mixed-in type's rendering - which, for an `IntEnum` on the wire, is often exactly right. Before an interpreter upgrade, the concrete audit is to search for enum members inside f-strings, `%`-format arguments, `str()` calls and `logging` arguments, and to check whether any downstream consumer parses or groups by the result. It is a small grep, and it is the difference between a noticed change and a silent one.

  • How do you get a stable human-readable label from an enum.IntEnum member?
    Ask for it explicitly. `member.name` gives `'HIGH'` and does not move when the interpreter changes how `str()` behaves; for user-facing text, define a `label` property or a separate mapping on the class. Reserve `str()` and bare f-string interpolation for the cases where you genuinely want the mixed-in type's rendering - which for an IntEnum being written to a wire format is often exactly right.
  • Why does repr() still show `<Priority.HIGH: 3>` when str() shows '3'?
    That split is the entire purpose of `enum.ReprEnum`, despite its name suggesting the opposite. It leaves `__repr__` as Enum's, so tracebacks, debuggers and `repr()`-based logging keep the class, name and value, while `__str__` and `__format__` come from the data type so ordinary formatting matches the raw value. You get diagnosability and protocol-compatible text from the same object.
  • What breaks when a service on 3.10 with IntEnum members in its log lines upgrades to 3.11?
    Every f-string, `%s` and `str()` over those members flips from `'Priority.HIGH'` to `'3'` at once, with no exception and usually no test failure. Anything downstream that parses those strings, groups by them, or uses them as cache or metric keys changes behaviour silently - dashboards split series, alert rules stop matching. The audit before the upgrade is a search for enum members inside format strings and logging arguments; the fix is `.name`.

It is a name badge with the number printed large. Your debugger still reads the full badge - <Priority.HIGH: 3> - but anything that just scans it into a log line or a URL now sees only the number.

saying these in an interview costs you the question

  • Says str() and repr() of an IntEnum member are the same
  • Thinks `class P(int, Enum)` and enum.IntEnum print identically
  • Believes enum.ReprEnum changes `__repr__` rather than preserving it
  • Assumes str() of an enum member is stable across Python versions
  • Builds a user-facing label from str(member) instead of .name
  • Thinks enum.ReprEnum can be used without a data-type mixin

context