Skip to content
← All articles

Flag and IntFlag: bitmasks that print, iterate and validate

The hand-rolled alternative is integer constants and `flags & PERM_READ`, which produces unreadable debug output, allows meaningless values, and offers zero introspection.

The version everyone writes first:

PERM_EXECUTE = 1
PERM_WRITE = 2
PERM_READ = 4

if flags & PERM_READ:
    ...

What it costs you: print(flags) says 6, a debugger says 6, a log line says 6. Nothing rejects from_octal(9999). There is no way to list the permissions, no way to ask which bits are set, and flags & PERM_READ evaluates to 4 rather than to a boolean, so if flags & PERM_READ == PERM_READ and if flags & PERM_READ behave differently in ways people get wrong.

enum.Flag fixes all of it:

from enum import Flag, auto


class Permission(Flag):
    EXECUTE = auto()      # 1
    WRITE = auto()        # 2
    READ = auto()         # 4
    RW = READ | WRITE     # 6, a named composite
    ALL = READ | WRITE | EXECUTE

auto() in a Flag yields successive powers of two, not successive integers — the one place auto() means something different from what it means in a plain Enum.

Expression Result

| Permission.READ | Permission.WRITE | Permission.RW (the declared composite) | | repr(...) | <Permission.RW: 6> | | Permission.READ in Permission.ALL | True | | list(Permission.RW) | [Permission.WRITE, Permission.READ] | | list(Permission) | canonical single-bit members only | | Permission(8) | ValueError |

Membership is in, not &. Permission.READ in flags is a boolean and reads as English. And iterating a flag value yields its constituent single-bit members, which makes “explain this permission mask” a one-liner.

Iterating the class yields only the canonical members — RW and ALL are composites of existing flags and are treated as aliases, so they are skipped. That is 3.11+ behaviour and it is what you want for “list the atomic permissions”.

💡Permission(8) raises ValueError but SomeIntFlag(8) might click to reveal

not. What governs that? The boundary setting. Flag defaults to STRICT, which raises on any bit that is not covered by a member. IntFlag defaults to KEEP, which preserves unknown bits so that values round-tripping through a C API or a wire protocol are not silently destroyed.

That is the right default for each: a Flag is a closed domain you defined, so an out-of-range value is a bug; an IntFlag usually mirrors something external where tomorrow’s kernel may define bit 8.

You can set it explicitly — class Perm(Flag, boundary=STRICT) — and it is worth doing on any flag whose values arrive from outside the program, because the default depends on which base class you picked rather than on where the data came from. Validate explicitly at the boundary as well; a range check with a message naming your domain beats a ValueError from the enum machinery.

@verify catches the mistakes at class creation

from enum import verify, UNIQUE, CONTINUOUS

@verify(UNIQUE)
class Permission(Flag): ...

UNIQUE (3.11) rejects accidental aliases — two names with the same value, which otherwise silently makes the second an alias of the first. CONTINUOUS rejects gaps in the value sequence, which catches a deleted member or a typo’d literal. Both run at class-creation time, so the failure is an import error rather than a wrong answer.

Note that UNIQUE and named composites are in tension: RW = 6 is an alias by the enum machinery’s definition. If you want both, declare composites outside the class body or accept that @verify(UNIQUE) is not for this class.

IntFlag escapes the type

IntFlag members are ints, which is what you want when the value crosses into a C library or a database column. The cost is that arithmetic on them degrades to plain int:

reveal_type(Perm.R | Perm.W)        # Perm
reveal_type(Perm.R + 1)             # int -- the type is gone

Once a value has escaped to int, nothing stops it being compared against an unrelated IntFlag, or being stored somewhere expecting a different domain. Use Flag unless the integer identity is genuinely required, and .value where you need the number.

💡A permission mask is stored in a database as an integer. Where click to reveal

should the conversion to and from Permission live? In exactly two functions, at the persistence boundary, and nowhere else.

def to_octal(permission: Permission) -> int:
    return permission.value

def from_octal(bits: int) -> Permission:
    if not 0 <= bits <= 7:
        raise ValueError(f"{bits} is not a 3-bit permission mask")
    return Permission(bits)

The temptation is to use IntFlag so that no conversion is needed. That works and it spreads the integer representation through the whole program: every function now accepts an int-compatible value, arithmetic silently escapes the type, and the range check has nowhere to live.

Two named functions cost four lines and buy you a single place where the external representation is known, a single place to change when a fourth permission bit is added, and a validating parser that turns a corrupt row into an error naming your domain rather than a mask with a bit nobody defined.