One user's permissions on a Unix file fit in three bits. Read is 4, write is
2 and execute is 1, so 6 means read and write. Code full of mode & 4 makes
the reader decode every number. enum.Flag gives each bit a name and keeps
the bit operations.
from enum import Flag, auto
class Permission(Flag):
EXECUTE = auto()
WRITE = auto()
READ = auto()
RW = READ | WRITE
ALL = READ | WRITE | EXECUTE
print(Permission.EXECUTE.value, Permission.WRITE.value, Permission.READ.value) # 1 2 4
print(Permission.READ | Permission.WRITE) # Permission.RW
print([member.name for member in Permission(5)]) # ['EXECUTE', 'READ']
print([member.name for member in Permission]) # ['EXECUTE', 'WRITE', 'READ']
In a Flag, auto() gives each new member the next bit, so the values go
1, 2, 4 and so on. RW and ALL give names to combinations of those bits.
Looping over a value lists the single bits it contains. Looping over the
class lists only the single-bit members, in the order you declared them.
If you store the numbers on disk or send them to another program, write
them out, as in READ = 4. Then reordering the class cannot change them.
Any bit, or all of them
flags = Permission.READ
print(bool(flags & Permission.RW)) # True, READ is one of RW's bits
print(Permission.RW in flags) # False, WRITE is missing
print(Permission.READ in Permission.ALL) # True
a & b keeps the bits that a and b share, and bool() of the result is
True if they share any bit. a in b is True only when every bit of a
is also in b. For a single bit, the two checks agree. For a combination
such as RW they differ, so pick the one you mean. Permission(0) asks for
no bits, so it is in every value.
Check the range before you convert
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)
print(from_octal(6)) # Permission.RW
print(Permission(-1)) # Permission.ALL
Permission(8) raises ValueError on its own, because 8 is a bit the class
does not have. Permission(-1) does not raise. Python reads -1 as "every bit
set" and returns ALL. Check the range yourself, so that your function
rejects negative numbers too.
from_octal takes an int, not a string of digits. The name comes from
file modes such as chmod 640, where each octal digit is one three-bit
group. There, 6 is the owner's read and write, 4 is the group's read, and 0
is nothing for everyone else.
Check your understanding
A user has Permission.READ. Does bool(user & Permission.RW) mean they can read and write?
Show answer
Hide answer
No. user & Permission.RW keeps the bits the two share, which here is
READ, so bool() gives True. That check passes if the user has any bit
of RW. To require both bits, write Permission.RW in user, which is
False here.
Use IntFlag when other code expects integers
from enum import IntFlag
class WirePermission(IntFlag):
READ = 4
WRITE = 2
incoming: int = 4
print(WirePermission.READ == incoming) # True
print(WirePermission.READ | WirePermission.WRITE) # 6
print(type(WirePermission.READ + 1)) # <class 'int'>
An IntFlag member is also an int. It equals the matching integer and
works with code that expects one. Bit operations give back an IntFlag, but
arithmetic such as + gives a plain int. An IntFlag keeps unknown bits
instead of raising, so WirePermission(8) works. Use Flag when
permissions should stay apart from ordinary numbers, and IntFlag when an
existing interface passes integers.
Going further
- The
enum.verifydecorator checks a class when Python defines it. WithUNIQUEit rejects two names with the same value. WithNAMED_FLAGSit rejects a combination that uses a bit with no name of its own. Flagraises on unknown bits andIntFlagkeeps them. Theboundaryclass argument picks another rule, such as dropping unknown bits.- In a plain
Enum,auto()counts 1, 2, 3. In aStrEnum, it gives the member's name in lower case. - See the enum documentation.