Skip to content

← Stdlib Mastery step 49 of 55

Medium Primitives

Flag: a permission mask that prints, iterates and refuses bad bits

Replace integer constants and flags & PERM_READ with an enum.Flag.

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


def to_octal(permission: Permission) -> int: ...
def from_octal(bits: int) -> Permission: ...      # ValueError outside 0..7
def solve(octals: list[int], probes: list[int]) -> dict[str, object]: ...

solve returns:

key value
"decoded" for each octal, sorted(member.name or "" for member in permission), or "ValueError"
"roundtrip" for each octal, to_octal(from_octal(bits)), or "ValueError"
"membership" for each probe, Permission.READ in from_octal(bits); False when the probe is out of range
"canonical" [m.name for m in Permission]

| "rw_is_composite" | (Permission.READ | Permission.WRITE) is Permission.RW | | "all_octal" | to_octal(Permission.ALL) |

What the integer version 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 or ask which bits are set. And flags & PERM_READ evaluates to 4 rather than to a boolean, so if flags & PERM_READ and if flags & PERM_READ == PERM_READ differ in ways people get wrong.

Three behaviours the tests pin down.

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

Membership is in, not &: Permission.READ in flags is a boolean and reads as English. Iterating a value yields its constituent single-bit members, so sorted(m.name for m in Permission(5)) is ["EXECUTE", "READ"] — “explain this mask” in one line.

Iterating the class yields only canonical members: RW and ALL are composites of existing flags, so the enum machinery treats them as aliases and skips them. Permission(0) iterates to nothing at all.

Validate explicitly. Flag defaults to boundary=STRICT, so Permission(8) raises anyway — but the message comes from the enum machinery. An explicit range check produces an error naming your domain, and it does not depend on a default that differs by base class (IntFlag defaults to KEEP, which preserves unknown bits so values round-tripping through a C API survive).

Why not IntFlag. Its members are ints, which is convenient at a database or C boundary, and arithmetic on them degrades to plain intPerm.R | Perm.W is a Perm, but Perm.R + 1 is an int and the domain is gone. Use Flag and convert with two named functions at the boundary.

Worth knowing but not tested: @verify(UNIQUE) and @verify(CONTINUOUS) (3.11) catch accidental aliases and gaps at class-creation time. UNIQUE is in tension with named composites, which the machinery regards as aliases.