What does enum.ReprEnum change about how enum.IntEnum members print?
answer
- The issue is text, not equality
- repr stays informative, str does not
- The mixed-in type supplies `__str__`
- Changed in Python 3.11
- Use `.name` for a stable label
basics
~10 senum.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 linesfrom 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
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.
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.
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.
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