skip to content

Why does enum.Flag raise on an int carrying bits no member names, while enum.IntFlag keeps them?

level: seniorimportance: should knowfreq 22%

answer

  1. There is a policy, not one behaviour
  2. It is set per class
  3. The two base classes differ by default
  4. Four settings: raise, strip, drop out, retain
  5. Formalised in 3.11

basics

~20 s

Since Python 3.11 each flag class has a boundary policy. enum.Flag defaults to STRICT, so an out-of-range value raises ValueError; enum.IntFlag defaults to KEEP, so unknown bits survive in the value but are invisible to iteration.

solid answer

~40 s

Python 3.11 formalised this with `enum.FlagBoundary`, set per class with the `boundary=` keyword. `enum.Flag` defaults to `STRICT`: `Perm(8)` when no member owns bit 8 raises `ValueError` naming the offending bits. `enum.IntFlag` defaults to `KEEP`: the value is accepted and the unnamed bit is preserved, so it round-trips, but iterating that value yields only the members it recognises — the extra bit is carried silently. The other two settings are `CONFORM`, which strips the unknown bits, and `EJECT`, which drops out of the enum entirely and hands back a plain `int`. The choice matters wherever flag values are persisted: `STRICT` turns a forward-compatibility problem into a loud, data-dependent `ValueError`, while `KEEP` preserves bits a newer writer added at the cost of them never showing up in any listing.

code

python · 21 lines
python
from enum import Flag, IntFlag, auto

class Perm(Flag):        # default boundary: STRICT
    READ = auto()
    WRITE = auto()
    EXEC = auto()

try:
    Perm(8)
except ValueError as exc:
    print("strict:", str(exc).splitlines()[0])

class IPerm(IntFlag):    # default boundary: KEEP
    READ = auto()
    WRITE = auto()

v = IPerm(9)
print("repr:", repr(v))       # <IPerm.READ|8: 9>
print("value:", v.value)      # 9 - the unnamed bit survives
print("members:", list(v))    # [READ] - but it is not listed
print("len:", len(v))         # 2 - len counts bits, not members

go deeper

for a junior

Know that constructing a flag from a raw integer can fail — enum.Flag raises ValueError when the number contains bits no member defines — and that enum.IntFlag is the lenient variant.

for a middle

Explain the four enum.FlagBoundary settings and the two defaults added in Python 3.11: STRICT for Flag, KEEP for IntFlag, with CONFORM stripping unknown bits and EJECT returning a plain int.

for a senior

Show the operational reasoning: what happens to stored values when a newer writer adds a bit, why the failure looks intermittent and data-dependent, where to convert integers into members, and how to test the unknown-bit path that fixtures never produce.

for a principal

Own the contract around persisted flag values — appended-only numbering, reserved bits, deploy ordering between writers and readers, and the standing rule that policy or deny bits are never handled by a setting that discards them silently.

## The problem the boundary solves A flag enum names some bits. A value handed to it — from a database column, a config file, a message on a queue — may have bits it does not name. Before Python 3.11 the handling of that case was ad hoc and differed between `enum.Flag` and `enum.IntFlag`. 3.11 gave it a name: `enum.FlagBoundary`, an enum of four policies chosen per class with a `boundary=` class keyword. ```python from enum import Flag, IntFlag, CONFORM, EJECT, KEEP, auto class Perm(Flag): # boundary defaults to STRICT READ = auto() # 1 WRITE = auto() # 2 EXEC = auto() # 4 ``` **`STRICT`** — the default for `Flag`. Any bit outside the named set is an error: ```python Perm(8) # ValueError: <flag 'Perm'> invalid value 8 # given 0b0 1000 # allowed 0b0 0111 ``` The message prints the given and allowed bit patterns, which makes it one of the more helpful errors in the stdlib. **`KEEP`** — the default for `IntFlag`. The value is accepted whole and the unnamed bits are retained: ```python class IPerm(IntFlag): READ = auto() WRITE = auto() v = IPerm(9) v # <IPerm.READ|8: 9> list(v) # [<IPerm.READ: 1>] - the 8 is invisible here v.value # 9 - but it round-trips ``` **`CONFORM`** — unknown bits are stripped and a valid member is returned: with only `A = 1` and `B = 2` defined, `Cfg(7)` yields `<Cfg.A|B: 3>`. **`EJECT`** — the value leaves the enum entirely: `Cfg2(7)` returns the plain `int` `7`, and `type()` of it really is `int`. Anything downstream that assumed it had a flag member now has a number, which is why this is the rarest choice. ## Why `IntFlag` is lenient by design `IntFlag` exists for values that come from outside Python — an OS bitmask, a protocol field, a legacy column. In that world the process does not own the bit vocabulary and cannot assume it is complete, so refusing an unknown bit would break a program that has no business caring about it. `KEEP` also means a value read and written back is unchanged, which is the correct default for a pass-through. Plain `Flag` is for vocabularies the program *does* own, and there an unknown bit is a bug worth surfacing immediately. ## The production shape of this Consider a fraud-scoring service that stores per-rule toggles as one integer column. An 11-person team ships a new rule and adds a fourth flag; that deployment starts writing values with the new bit set. Any instance still running the previous code now reads those rows. With a plain `Flag`, the read raises `ValueError` — but only for the rows written since the new deployment, so it presents as a partial, data-dependent failure that looks intermittent from the outside and disappears entirely on a fixture-based test suite. That is loud in the right way, provided someone can act on it. With `IntFlag` under `KEEP`, nothing raises, the extra bit round-trips safely, and the only symptom is that a screen listing active rules never mentions the fourth one — because iteration yields named members only. Neither behaviour is wrong; shipping without deciding which one you want is. The decisions worth stating in an interview: * **Choose the boundary deliberately** and write it down: `class Perm(Flag, boundary=KEEP)` is one keyword and it documents the intent. * **Never let `CONFORM` near a permission or a deny bit.** Silently discarding a bit that meant "this rule is disabled" is a security-shaped failure, and it leaves no trace in the value. * **Version the vocabulary if the ints are persisted.** Reserved bits, a schema version alongside the value, or a rule that flags are only ever appended and never renumbered. * **Validate at the edge.** Convert the integer to a member once, at the boundary of the system, where a `ValueError` can be turned into a real error response — not deep inside a request handler. * **Test the unknown-bit path**, because normal fixtures never produce one; construct a value with a bit above the highest named member and assert on the behaviour you chose. ## Two details that catch people Iteration yields named members only, but `len()` counts the bits that are set — so under `KEEP` a value can report `len()` of 2 while iterating it produces exactly one member. The extra bit is present in `value`, in `len()` and in the `repr` (which shows it as a bare number, `<IPerm.READ|8: 9>`), and absent from every listing built by iterating. And the boundary applies to *construction*, not to the operators: combining two valid members can never produce an out-of-range value, so the check only fires where a raw value enters the enum. ## Version notes `enum.FlagBoundary` with `STRICT`, `CONFORM`, `EJECT` and `KEEP`, the `boundary=` keyword, and the current defaults (`STRICT` for `Flag`, `KEEP` for `IntFlag`) all arrived in Python 3.11 and are unchanged on 3.14.

  • Under KEEP, why does an unnamed bit not appear when you iterate the value?
    Iteration yields canonical members — those naming a single, defined bit — so a bit no member owns has nothing to yield. It still lives in `value`, the `repr` shows it as a bare number, and `len()` counts it because `len()` is the number of bits set. So `len(v)` can be 2 while `list(v)` has one entry. That asymmetry is the trap: a permissions screen built from iteration silently omits a bit the value is still carrying.
  • Why is `CONFORM` a poor choice for a permission or policy flag set?
    It discards bits it does not recognise and returns a value that looks perfectly valid, so a bit meaning "this rule is disabled" or "deny" can vanish with no error and no trace. Data written back afterwards is silently lossy. `STRICT` fails loudly and `KEEP` preserves; `CONFORM` is for genuinely lenient parsing of input you are free to sanitise.
  • Where in a service would you convert a stored integer into a flag member?
    Once, at the system boundary — the repository row mapper, the deserializer, the request parser — so a `ValueError` from `STRICT` becomes a handled error with context about which record failed. Converting deep inside business logic scatters the failure across call sites and usually produces a 500 rather than a diagnosis.
  • How do you keep persisted flag values safe as the flag set grows?
    Treat the numbering as a contract: append new bits, never renumber or reuse a retired one, and record the mapping somewhere reviewable. If old readers must tolerate new bits, choose `KEEP` deliberately; if they must not, choose `STRICT` and make deploy order part of the rollout plan. Either way, test a value carrying a bit above the highest named member.

saying these in an interview costs you the question

  • Saying enum.Flag and enum.IntFlag treat unknown bits identically
  • Claiming an unnamed bit is always silently dropped
  • Believing the boundary check also applies to | and &
  • Assuming iteration under KEEP reports the unnamed bit
  • Thinking EJECT still returns an enum member
  • Using CONFORM for permissions to avoid handling errors

context