skip to content

Aliases, unique and _missing_

A repeated value silently creates an alias instead of a new member, enum.unique forbids it, and _missing_ turns an unknown value into a fallback. Interviewers ask why iteration skipped a member.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

3

Why does an Enum member defined with a duplicate value never appear in iteration?

level: juniorimportance: must knowfreq 45%

answer

  1. The class never gained a second member
  2. Two names, one underlying value
  3. Canonical member versus extra spelling
  4. Iteration hides it, __members__ keeps it
  5. A decorator can forbid it outright

basics

~20 s

A duplicate value creates an alias, not a new member: enum.Enum binds the second name to the member that already owns that value. Iterating the class yields only canonical members, so the alias name never shows up.

solid answer

~50 s

When the body of an `enum.Enum` subclass assigns a value that an earlier member already holds, no second member is created — the new name becomes an **alias** bound to the first member. Writing `PENDING = 1` after `QUEUED = 1` makes `PickStatus.PENDING is PickStatus.QUEUED` true, and `PickStatus.PENDING.name` reports `'QUEUED'`, because there is only one object. Iteration, `list()` and `len()` over the class walk the canonical members only, which is why the alias seems to vanish; the class's `__members__` mapping is the complete name-to-member view and does include it, as does lookup by name. That is usually intentional — one state with two spellings — but when it is a copy-paste slip it is silent. Decorate the class with `@enum.unique` to turn a repeated value into a `ValueError` while the class is being created; since 3.11, `enum.verify` with `enum.EnumCheck.UNIQUE` runs the same check.

code

python · 11 lines
python
import enum

class PickStatus(enum.Enum):
    QUEUED = 1
    PENDING = 1        # same value as QUEUED, so it becomes an alias
    PICKED = 2

print(list(PickStatus))                    # only the canonical members
print(PickStatus.PENDING is PickStatus.QUEUED)
print(list(PickStatus.__members__))        # every defined name, aliases included
print(PickStatus['PENDING'], PickStatus(1).name)

go deeper

for a junior

Recall that repeating a value gives the first member a second name rather than creating a new member, and that iterating the class shows only the canonical ones. Being able to say 'that is an alias' is most of the answer.

for a middle

Be ready to explain the mechanics: the first name to claim a value owns the member, later names bind to that same object, members keeps every name while iteration and len() do not, and @enum.unique converts the duplicate into a ValueError at class creation.

for a senior

Show the judgement call — decide whether the aliases in a codebase are deliberate synonyms or a merge accident, default to @enum.unique on enums backing persisted codes, and remember that an alias is invisible to any code that drives itself by iterating the class.

for a principal

Own the convention: whether aliases are a supported way to rename a constant without breaking callers or are banned outright, and how that interacts with stored values, database constraints and contracts with other services.

### What an Enum class body actually builds An `enum.Enum` subclass is not an ordinary class. Its body executes inside a special namespace supplied by the enum machinery, and that namespace records each plain assignment as a candidate member in definition order. When the body finishes, the machinery creates **one member object per distinct value** and builds two mappings behind the class: a name-to-member mapping, exposed read-only as `__members__`, and a value-to-member mapping used by the call syntax `Cls(value)`. The value mapping is the whole story. The first name to claim a value becomes that value's *canonical* member. A later name whose value compares equal to an already-claimed one gets no member of its own — it is bound, both as a class attribute and in the name mapping, to the member that already exists. That second name is an **alias**. ```python import enum class PickStatus(enum.Enum): QUEUED = 1 PENDING = 1 PICKED = 2 assert PickStatus.PENDING is PickStatus.QUEUED assert PickStatus.PENDING.name == 'QUEUED' assert repr(PickStatus.PENDING) == '<PickStatus.QUEUED: 1>' ``` There is no second object anywhere. `PickStatus.PENDING` and `PickStatus.QUEUED` are the same singleton, they hash and compare identically, they pickle back to the canonical member, and the member's own `name` attribute reports `'QUEUED'` because that is the name the member was created with. ### Where the alias shows and where it hides Iteration is the surprising half. `for m in PickStatus`, `list(PickStatus)`, `len(PickStatus)` and any comprehension over the class walk the **canonical members only** — two members here, not three. That is deliberate: iteration is meant to enumerate the distinct states, and an alias is not a distinct state. Name-based access is the other half, and it keeps everything. `PickStatus['PENDING']` succeeds, `PickStatus.PENDING` succeeds, and `PickStatus.__members__` — a read-only mapping of every defined name in definition order — contains `QUEUED`, `PENDING` and `PICKED`, with the first two pointing at the same object. So `len(PickStatus.__members__)` can exceed `len(PickStatus)`, and the difference is exactly the alias count. Value lookup collapses back to canonical: `PickStatus(1)` returns the `QUEUED` member no matter how many names claim the value 1. ### Why the language allows it Aliases are a feature before they are a hazard. They let one state carry two vocabularies — the word the domain uses and the word an upstream system uses — and they let a constant be renamed without breaking callers, since the old spelling keeps resolving to the same member. Some code even relies on it: the canonical name is the one that serializes, so a rename that keeps the old name as an alias is a source-compatible change with no change to stored data. ### Why it bites The hazard is that it is *silent*. A copy-pasted line or a bad merge that repeats a value produces a class that imports cleanly and behaves subtly wrongly: a validation loop that iterates the class never checks the aliased state, a set of choices generated by iterating the class is missing an option, and an audit that compares `len(Cls)` against a stored code table is off by one. Nothing raises, because from the enum's point of view nothing is wrong. Note the asymmetry with duplicate **names**: repeating a name is loud, not silent. A body containing `A = 1` twice raises `TypeError: 'A' already defined as 1`, because the enum namespace refuses to rebind a name it has already recorded. Only duplicate *values* are quiet. ### Making duplicates an error Decorate the class with `@enum.unique` when aliases are not wanted: ```python import enum try: @enum.unique class Bad(enum.Enum): QUEUED = 1 PENDING = 1 except ValueError as exc: print(exc) # duplicate values found in <enum 'Bad'>: PENDING -> QUEUED ``` The check runs while the class object is being created, so the failure surfaces at import time with the offending pair named — not at some later call site. Since 3.11, `enum.verify` with `enum.EnumCheck.UNIQUE` runs the same uniqueness check and can be combined with other structural checks on the class. Neither is on by default, and neither should be applied blindly: on an enum whose aliases are deliberate synonyms, `@enum.unique` is simply wrong. A reasonable house rule is to require `@enum.unique` on enums whose values are persisted or exchanged across a boundary — where a duplicate is almost always a mistake — and to leave it off, with a comment naming the intent, where the synonym is real. ### What an alias is not An alias is created once, at class-definition time, from a value already present in the body. It is not a runtime fallback: turning an unrecognised value into a member when the lookup happens is a different mechanism entirely. And it is not a way to attach extra data to a member — a second name carries no state of its own, because there is no second object to carry it.

  • How do you make a duplicate enum value a hard error instead of a silent alias?
    Decorate the class with `@enum.unique`. It runs while the class object is being created and raises `ValueError`, naming the offending pair, e.g. `duplicate values found in <enum 'Bad'>: PENDING -> QUEUED`. Since 3.11, `@enum.verify(enum.EnumCheck.UNIQUE)` performs the same uniqueness check and can be combined with other structural checks. Both fail at import time rather than at some later call site, which is exactly where you want a copy-paste slip to surface.
  • How do you list every name on an Enum class, aliases included?
    Read `__members__` on the class: a read-only mapping of every defined name to its member, in definition order, aliases included. Iterating the class, `len()` and comprehensions over it use canonical members only, so `len(Cls.__members__)` can exceed `len(Cls)` — and the difference is exactly the number of aliases.
  • What happens if you repeat a member name instead of a value?
    That is loud, not silent: `TypeError: 'A' already defined as 1` while the class is being created. The enum class body uses a namespace that refuses to rebind a name it has already recorded, so only duplicate *values* are quiet. It is a useful asymmetry to state in an interview, because it shows you know the aliasing rule is about values, not names.

A nickname does not create a second colleague: 'Kate' and 'Katherine' reach the same person, and the staff roster prints only the name on the payroll.

saying these in an interview costs you the question

  • Says the class has two distinct members sharing one value
  • Thinks the alias is a separate object with its own identity
  • Expects len() on the class to count alias names
  • Believes a repeated value raises an error by default
  • Thinks lookup by the alias name fails
  • Confuses a definition-time alias with a runtime fallback

context

open as a page

How do you look up an Enum member by name versus by value, and what does each raise?

level: middleimportance: should knowfreq 50%

basics

~10 s

Calling the class looks up by value, as in Bin(2), and raises ValueError when no member holds that value. Subscripting looks up by name, as in Bin['STAGING'], and raises KeyError. Attribute access raises AttributeError.

open as a page

How does defining _missing_ on an Enum change a failed value lookup?

level: seniorimportance: should knowfreq 32%

basics

~10 s

missing is a classmethod the Enum class calls when a value lookup finds no member. Return a member of that enum to make the lookup succeed, or None to let the original ValueError stand.

open as a page