skip to content

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

level: seniorimportance: should knowfreq 32%

answer

  1. A hook on the failing lookup path
  2. Only the call syntax reaches it
  3. Its return value decides what the caller sees
  4. None keeps the original ValueError
  5. A classmethod returning a member of cls

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.

solid answer

~50 s

`_missing_` is a `classmethod` on the enum class, called from the **call syntax only** — `PickStatus(value)` — after the value-to-member mapping has already missed. Return a member of that enum and the lookup succeeds transparently; return `None` and the original `ValueError` is raised; return anything else and you get a `TypeError` saying the hook returned something that is not `None` or a valid member. An exception raised inside it propagates, with the `ValueError` as its context. It backs the value door only: `PickStatus['BOGUS']` still raises `KeyError` without calling it. The good use is a small, total normalisation — case folding, whitespace, a fixed table of retired codes. The bad use is a catch-all that maps anything unknown onto a sentinel member, because that silently deletes input validation; make that leniency explicit at the call site instead.

code

python · 19 lines
python
import enum

class PickStatus(enum.Enum):
    QUEUED = "queued"
    PICKED = "picked"
    SHORT = "short"

    @classmethod
    def _missing_(cls, value):
        if isinstance(value, str):
            return cls.__members__.get(value.strip().upper())
        return None

print(PickStatus("picked"))      # exact value: _missing_ is never reached
print(PickStatus(" Picked "))    # legacy feed spelling: _missing_ maps it
try:
    PickStatus("lost-in-transit")
except ValueError as exc:
    print(type(exc).__name__, exc)

go deeper

for a junior

Know that an enum can define a fallback for values it does not recognise, and that without one an unknown value simply raises ValueError. You are unlikely to be asked to write the hook itself yet.

for a middle

Be able to write it correctly: a classmethod taking the value, returning a member of cls or None, and nothing else. Say which lookup triggers it and what each possible return does to the caller.

for a senior

Show operational judgement — a small total normalisation belongs in the type, a catch-all does not, the hook sits on a path other people's code walks, and both the accepted-alias and still-rejected cases need tests.

for a principal

Own the contract question: adding missing widens what the type accepts across every consumer, so decide where that tolerance belongs — at the boundary that ingests foreign data, or in the domain type everyone shares — and how a code retirement is scheduled and removed.

### The hook `_missing_` is a classmethod you define on an `enum.Enum` subclass. The class calls it from the **call syntax only** — `PickStatus(value)` — and only after the value-to-member mapping has already failed to find a member. It receives the value that was passed in, and its return value decides what the caller sees. The contract is narrow, and it is enforced: * **Return a member of this enum** and the lookup succeeds; the caller gets that member and never learns a fallback happened. * **Return `None`** and the original `ValueError` is raised, with the usual `'lost-in-transit' is not a valid PickStatus` message. `None` means "I have nothing", not "return `None` to the caller". * **Return anything else** — a raw string, an int, a member of a different enum — and you get a `TypeError` reading `error in PickStatus._missing_: returned ... instead of None or a valid member`. This is a common bug: returning the *value* you normalised instead of the member. * **Raise inside `_missing_`** and your exception propagates to the caller, with the `ValueError` attached as its context. Useful for a deliberate domain error; unpleasant when it is accidental, because the traceback now points into the enum rather than the call site. Because it is a `classmethod`, `cls` is the enum class, which is how a `_missing_` inherited from a mixin base can serve several enums. ### A worked example A warehouse pick-list builder ingests status codes from a legacy feed that spells them in upper case, sometimes with stray whitespace, while the enum stores lower-case values: ```python import enum class PickStatus(enum.Enum): QUEUED = "queued" PICKED = "picked" SHORT = "short" @classmethod def _missing_(cls, value): if isinstance(value, str): return cls.__members__.get(value.strip().upper()) return None assert PickStatus(" Picked ") is PickStatus.PICKED ``` Every call site that already writes `PickStatus(code)` now tolerates the feed's spelling, with no change at the call sites and no wrapper function to remember. Note what the fallback does here: it normalises and then resolves through `__members__`, the name mapping — so it deliberately accepts a *name* on the value door. That is a choice, and it is worth a comment in the code, because it makes the two lookups less distinct than the language made them. ### What it does not do `_missing_` backs the value door only. `PickStatus['BOGUS']` still raises `KeyError` without ever calling it, attribute access still raises `AttributeError`, and a membership test still just answers `True` or `False`. If you want lenient name lookup, write it against `__members__`. It is also not the alias mechanism. An alias is created once, when the class body runs, by repeating a value; `_missing_` runs at lookup time and creates nothing by default. (A member it hands back should already exist — the standard library's flag enums use `_missing_` to build cached composite pseudo-members, which is an advanced pattern: if you invent objects there without caching them, `is` comparisons and pickling stop behaving like an enum.) ### When it is the right tool, and when it is not `_missing_` is at its best for a **stable, total normalisation**: case folding, whitespace, a fixed table of retired codes that map onto current members. The rule is small, it is the same everywhere, and hiding it inside the type is genuinely better than repeating it at fifty call sites. It is at its worst as a **catch-all**. A `_missing_` that returns an `UNKNOWN` member for anything it does not recognise deletes your input validation: malformed codes stop raising, flow into the pick-list, and surface much later as a state nothing knows how to handle. If unknown input must be tolerated, make that visible at the call site — a named classmethod parser that takes an explicit default, or an explicit `try`/`except ValueError` — so a reader can see leniency being chosen rather than inheriting it from the type. Three more operational points. It sits on the miss path of every lookup, including lookups deep in library code you did not write, so it must be cheap and side-effect free — no I/O, no logging that can throw, no network. It must be deterministic, or the same input will resolve differently in two processes. And because it changes what the constructor accepts, it widens the type's contract: the set of values that produce a member is no longer the set written in the class body, so document it, and test both the accepted-alias path and the still-rejected path — the second test is the one that catches a catch-all creeping in later.

  • What happens if _missing_ returns the normalised string instead of a member?
    The class rejects it with `TypeError: error in PickStatus._missing_: returned 'picked' instead of None or a valid member`. Only two returns are accepted: a member of that same enum, or `None`. Returning the value you just normalised, rather than resolving it to a member, is the most common bug in a hand-written hook.
  • Does _missing_ run for PickStatus['BOGUS']?
    No. The subscript form goes straight to the name-to-member mapping and raises `KeyError` without consulting the hook, and attribute access raises `AttributeError`. `_missing_` backs the call syntax only. If you want lenient lookup by name, write it yourself against `__members__`.
  • When would you prefer an explicit classmethod parser over _missing_?
    When leniency should be visible where it is used. `_missing_` widens the constructor for every caller, including library code you did not write, so a catch-all there quietly removes validation. A named classmethod that takes an explicit default — or a plain `try`/`except ValueError` — shows a reader that tolerance was chosen, and leaves the strict lookup available.

It is the receiving clerk who recognises a supplier's old code and points at the right bin — helpful for a known list of retired codes, dangerous the moment they start guessing at anything unfamiliar.

saying these in an interview costs you the question

  • Thinks _missing_ also backs name lookup or attribute access
  • Returns a raw value or string instead of a member
  • Assumes returning None makes the call return None
  • Defines _missing_ as an instance method
  • Uses a catch-all that maps any junk to a sentinel member
  • Puts logging, I/O or other side effects on the miss path

context