skip to content

How do enum.Enum members compare with `is` and `==`, and why?

level: juniorimportance: must knowfreq 60%

answer

  1. Members are built once
  2. Every access returns the same object
  3. Equality quietly falls back to identity
  4. `Color.RED == 1` is False
  5. Mixins are where `is` and `==` split

basics

~20 s

Each enum.Enum member is created once and cached, so every access returns the same object and is compares them exactly. A plain member equals only itself: Color.RED == 1 is False, and so is a same-valued member of another enum.

solid answer

~40 s

`enum.Enum` builds every member once, when the class is created, and hands back that same instance forever after - `Color.RED`, `Color['RED']` and `Color(1)` are the identical object, because calling the class is a lookup rather than a construction. Members being singletons is why `is` is the idiomatic comparison. A plain `Enum` also does not define `__eq__`, so equality falls back to identity and agrees with `is`: `Color.RED == 1` is `False` (the 1 lives in `.value`), and `Color.RED == Shade.RED` is `False` even when both hold 1. That isolation is the point of an enum. Members are hashable and usable as dict keys; they are unordered, so `<` raises `TypeError` unless you mix in a data type such as `enum.IntEnum`. With those mixed-in kinds, `==` and `is` genuinely diverge.

code

python · 12 lines
python
from enum import Enum

class Color(Enum):
    RED = 1
    GREEN = 2

assert Color.RED is Color(1) is Color["RED"]
assert Color.RED != 1 and Color.RED.value == 1
try:
    Color.RED < Color.GREEN
except TypeError as exc:
    print("unordered:", exc)

go deeper

for a junior

Recall that a plain enum.Enum member is a one-of-a-kind object and that is is the idiomatic comparison. Know that Color.RED == 1 is False and that .value is how you reach the underlying 1.

for a middle

Be ready to explain the mechanism: the enum machinery builds each member once at class creation, every access returns that cached instance, and a plain Enum inherits object's identity-based equality, so == and is agree.

for a senior

Show where the guarantee ends. A mixed-in kind such as enum.IntEnum equals raw values, so is and == diverge; identity is scoped to one imported class object, so anything crossing an interpreter or re-import boundary must travel as .name or .value.

for a principal

Own the convention that enum members, not bare ints or strings, are the domain vocabulary, and that conversion to and from raw values happens at exactly one boundary per system rather than being sprinkled through call sites.

### Members are built once, and only once When the body of an `enum.Enum` subclass finishes executing, the enum machinery walks the plain assignments in that body and turns each one into a **member**: an instance of the class being created, carrying a `.name` (the attribute you wrote) and a `.value` (what you assigned). Those instances are created exactly once, at class-creation time, and then cached on the class. Every later route to a member hands back that same object: ```python Color.RED # attribute access Color["RED"] # lookup by name Color(1) # lookup by value - a LOOKUP, not a constructor call list(Color)[0] # iteration, in definition order ``` All four are the identical object, and `Color.RED is Color(1) is Color["RED"]` is `True`. There is no path that produces a second `Color` instance holding 1. That is what "members are singletons" means, and it is the property `is` rests on. ### Why identity is the idiomatic comparison A plain `Enum` does not define `__eq__`, so it inherits `object`'s, which *is* identity. `==` and `is` therefore agree, and `is` says the intent out loud: you are asking "is this that member?", not "does this value happen to match?". It is also marginally faster - no method dispatch - and it does not tempt you into writing `stage == "build"`, which for a plain enum is quietly `False` forever. ### Where equality is not identity Two cases. First, a member never equals its raw value: `Color.RED == 1` is `False`, because the member is an instance of `Color`, not an `int`; the 1 lives in `.value`. Second, members of two *different* enum classes never compare equal even with the same value - `Color.RED == Shade.RED` is `False`. That second property is most of the reason to use an enum at all: a value drawn from one vocabulary cannot silently pass as a value from another. The exception is the mixed-in kinds. `enum.IntEnum` and `enum.StrEnum` members really are `int` and `str` subclass instances, so they inherit that type's `__eq__` and *do* equal their raw values and each other across classes. There, `==` and `is` genuinely diverge: `Priority.HIGH == 3` is `True` while `Priority.HIGH is 3` is not. That divergence is the whole reason to know which kind you are holding. ### Ordering, hashing, truthiness Plain members are unordered. `Color.RED < Color.GREEN` raises `TypeError: '<' not supported between instances of 'Color' and 'Color'`, and `sorted(Color)` fails for the same reason - sort with an explicit key such as `sorted(Color, key=lambda m: m.value)`, or rely on iteration, which yields members in **definition order**, not value order. Members are hashable and make excellent dict keys and set elements. A plain member hashes by its name, so `{Color.RED: "stop"}[Color.RED]` works while `[1]` raises `KeyError`. Truthiness is a sharp edge worth remembering: a plain member is always truthy, whatever its value, because nothing overrides the default truthiness of an object; but a member of an `IntEnum` with value 0 is **falsy**, because there `bool()` is the integer's. `if priority:` therefore means two different things depending on which kind you chose. ### Identity across copies, pickles and processes Enum defines its own reduction, so pickling a member stores the class plus the value and unpickling performs a lookup - you get the existing member back, and `is` still holds. `copy.copy` and `copy.deepcopy` likewise return the member itself. So identity survives queues, caches and round-trips inside one interpreter. The guarantee is scoped to *one imported class object*. If the same module is somehow imported twice under different names, or the member crosses into a separate interpreter or a subprocess that re-imports it, you are comparing members of two distinct class objects and identity fails. When a member has to cross such a boundary, send `.value` (or `.name`) and rebuild the member on the far side by calling the class. ### What to say in an interview "Enum members are created once and cached, so every access to `Color.RED` returns the same object. That makes `is` the natural comparison, and since a plain Enum does not override `__eq__`, equality is identity anyway. A plain member is deliberately not equal to its raw value - `Color.RED == 1` is `False` - and not equal to a same-valued member of another enum. If I need equality with raw values I opt into it explicitly with `enum.IntEnum` or `enum.StrEnum`, knowing that it costs me exactly that isolation."

  • What value does `enum.auto()` give the first member of a plain enum.Enum class?
    1. For a plain `Enum`, `auto()` continues from the last numeric value and starts at 1, so three consecutive `auto()` members get 1, 2 and 3. The value is resolved once, at class creation, so the member is still a singleton with a fixed `.value`. Subclasses can redefine that generation: in `enum.StrEnum`, `auto()` produces the lower-cased member name instead.
  • Does pickling or deep-copying an enum.Enum member break the `is` comparison?
    No. Enum defines its own reduction, so unpickling looks the member up by value on the class and returns the existing instance; `copy.copy` and `copy.deepcopy` return the member itself. Identity therefore survives queues, caches and round-trips inside one interpreter. It is scoped to a single imported class object, though - if the module is re-imported under another name or the value crosses into a separate interpreter, you are comparing members of two different classes and must compare `.name` or `.value` instead.
  • Why is `Color.RED == 1` False when the member's `.value` is 1?
    Because the member is an instance of `Color`, not an `int`; the 1 is stored on it as `.value`. A plain `Enum` does not override `__eq__`, so equality is identity, and 1 is a different object of a different type. If the member must equal its raw value, opt into that with `enum.IntEnum` or `enum.StrEnum` - and accept that you also lose the isolation from other enums. Otherwise compare `member.value == 1` explicitly.

saying these in an interview costs you the question

  • Says `is` on enum members is unreliable and only `==` is safe
  • Believes a plain enum.Enum member equals its raw value
  • Thinks same-valued members of different enum classes compare equal
  • Assumes plain enum.Enum members support `<` ordering
  • Claims each attribute access constructs a fresh member object
  • Confuses `.value` with `.name` when reading a member

context