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.