Some values represent one choice from a fixed set. A log level might be INFO, WARNING, or ERROR. Python’s Enum is a natural fit because one value should identify one member.
Other values represent a combination of independent options. A file operation may allow reading and writing. A protocol field may enable compression and encryption. A component may expose several capabilities at the same time.
Representing those combinations as ordinary booleans works at first:
permissions = {
"read": True,
"write": True,
"execute": False,
}But this representation becomes awkward when the value must be passed around as one object, compared with predefined combinations, stored as a compact integer mask, or combined with bitwise operators.
Python’s enum.Flag and enum.IntFlag are designed for this problem. They let each independent option occupy one bit, then represent a combination as the bitwise union of those bits.
The key mental model is: a flag value is a set encoded in bits, and bitwise operators perform set-like operations on those bits.
Start with independent powers of two
A bit flag works because each independent option uses a different power of two:
READ 001
WRITE 010
EXECUTE 100No two options share a bit. Combining READ and WRITE with bitwise OR produces:
READ 001
WRITE 010
---
OR 011The result 011 preserves enough information to tell that both original options are present.
Python can assign these values for you with auto():
from enum import Flag, auto
class Permission(Flag):
READ = auto()
WRITE = auto()
EXECUTE = auto()For Flag, auto() assigns successive powers of two. You normally care about the distinct bits, not the exact numbers.
Create a combination with |:
read_write = Permission.READ | Permission.WRITEread_write is still a Permission value. It represents the two enabled flags together.
That is the first important difference from manually manipulating arbitrary integers: the result keeps the domain type.
Read bitwise operators as set operations
The four important operators are OR, AND, XOR, and inversion.
OR adds flags to a combination
Use | when a result should contain everything enabled by either operand:
permissions = Permission.READ | Permission.WRITE
permissions |= Permission.EXECUTEAfter the second line, all three flags are enabled.
AND keeps only shared flags
Use & to ask which flags two values have in common:
permissions = Permission.READ | Permission.WRITE
writable = permissions & Permission.WRITEwritable is Permission.WRITE.
If there is no overlap, the result is the zero-valued flag:
overlap = Permission.READ & Permission.EXECUTE
print(bool(overlap))
# FalseA flag with no bits set is false in a boolean context. This makes intersections convenient in conditions, although explicit membership is often easier to read.
XOR toggles differing flags
Exclusive OR, written ^, keeps bits that are set in exactly one operand:
permissions = Permission.READ | Permission.WRITE
permissions ^= Permission.WRITEThe result is now only Permission.READ.
XOR is useful for deliberate toggling. It is a poor choice when code should simply ensure a flag is enabled or disabled, because the outcome depends on the previous state.
Inversion selects the other defined flags
~ returns the flags from the same enumeration that are not present:
permissions = Permission.READ | Permission.WRITE
other = ~permissionsWith the three-member Permission class above, other is Permission.EXECUTE.
Think of inversion as complementing against the flags defined by the type, not as producing an arbitrary negative integer bit pattern.
Prefer membership when the question is “is this flag enabled?”
A common low-level bitmask test looks like this:
if permissions & Permission.WRITE:
...It works because the intersection is truthy when the WRITE bit is present.
Flag also supports membership syntax:
if Permission.WRITE in permissions:
...This expresses the question directly.
Membership works for combinations too:
required = Permission.READ | Permission.WRITE
available = Permission.READ | Permission.WRITE | Permission.EXECUTE
if required in available:
print("all required permissions are available")The condition is true only when every bit in required is present in available.
This distinction matters. Testing a multi-flag requirement with ordinary truthiness can accidentally accept a partial match:
required = Permission.READ | Permission.WRITE
available = Permission.READ
if available & required:
print("this runs even though WRITE is missing")The intersection is nonzero because READ overlaps.
Use membership when all requested flags must be present:
if required in available:
print("all required flags are present")The operator should match the actual question: intersection asks whether there is overlap; membership asks whether one flag set is contained in another.
Use Flag when values should stay inside the domain
Flag is deliberately stricter than an integer bitmask.
A plain Flag member is not an int:
isinstance(Permission.READ, int)
# FalseThat separation is useful when the flags are an application-level concept rather than a numeric protocol field.
For example:
def can_publish(permissions: Permission) -> bool:
required = Permission.READ | Permission.WRITE
return required in permissionsThe type communicates that callers should provide a Permission, not an unrelated integer that happens to contain some bits.
This does not create runtime type enforcement by itself; Python type annotations are not automatic argument validators. The benefit is a clearer model for readers, type checkers, and API boundaries.
Choose Flag when arithmetic and arbitrary integer interoperability would be mistakes rather than conveniences.
Use IntFlag when integer interoperability is part of the boundary
IntFlag has the same flag-combination model, but its members are also integers.
That is useful when an external interface already represents flags numerically:
from enum import IntFlag
class WirePermission(IntFlag):
READ = 1
WRITE = 2
EXECUTE = 4You can combine members and convert the result to an integer:
permissions = WirePermission.READ | WirePermission.WRITE
encoded = int(permissions)
print(encoded)
# 3And you can construct the flag value again from that integer:
decoded = WirePermission(3)
print(WirePermission.READ in decoded)
# TrueThis is practical for operating-system APIs, binary formats, database columns, and other interfaces where the numeric mask is part of the contract.
The trade-off is weaker separation from ordinary numbers.
Because IntFlag is an int subclass, non-bitwise integer operations can discard the enum type:
value = WirePermission.READ + 2
print(value)
# 3The result is an ordinary integer, not a WirePermission.
Use IntFlag because a numeric boundary requires it, not merely because integers feel familiar.
Keep stable external bit values explicit
auto() is convenient when numeric values are internal implementation details:
class Feature(Flag):
CACHE = auto()
TRACE = auto()
RETRY = auto()But automatically assigned values should not become an accidental storage or wire-format contract.
Suppose a database stores Feature.CACHE | Feature.RETRY as an integer. If a future edit changes member definitions or their ordering, relying on implicit values makes compatibility harder to reason about.
When numeric values cross a persistent or external boundary, define them explicitly:
class ProtocolFeature(IntFlag):
COMPRESSION = 1
ENCRYPTION = 2
CHECKSUM = 4Now the bit assignments are visible API decisions.
The same principle applies to any serialized enum value: persistence turns an implementation detail into a compatibility requirement. Make that requirement explicit in code.
Unknown bits need an intentional policy
External numeric masks can contain bits your current program does not recognize.
With IntFlag, unknown bits can remain in the value:
unknown = WirePermission(8)
print(unknown.value)
# 8This behavior can be useful for forward compatibility. A newer producer may set a bit that an older consumer does not yet understand, and preserving the raw mask can prevent accidental information loss.
But preservation is not the same as understanding.
Code should not treat unknown bits as recognized capabilities. If accepting only known flags is a correctness or security requirement, validate the mask before acting on it.
One simple approach is to compute the known mask:
KNOWN_PERMISSIONS = (
WirePermission.READ
| WirePermission.WRITE
| WirePermission.EXECUTE
)
def has_unknown_bits(raw: int) -> bool:
return bool(raw & ~int(KNOWN_PERMISSIONS))Then validate values received from an untrusted or compatibility-sensitive boundary:
raw = 9
if has_unknown_bits(raw):
raise ValueError("unsupported permission bits")
permissions = WirePermission(raw)The correct policy depends on the interface. A protocol proxy may need to preserve unknown bits. An authorization decision may need to reject them. Make that decision explicit instead of assuming construction alone performs validation.
Named combinations are aliases, not new independent bits
You can give a common combination a name:
class Permission(Flag):
READ = 1
WRITE = 2
EXECUTE = 4
READ_WRITE = READ | WRITENow both expressions describe the same value:
Permission.READ_WRITE == Permission.READ | Permission.WRITE
# TrueThis can improve readability for combinations that have domain meaning.
However, READ_WRITE is not a new independent flag. It is a name for a combination of existing bits.
That affects iteration:
list(Permission)The normal iteration exposes the canonical single-bit members rather than treating every named combination as another independent option.
This is helpful because otherwise combinations would blur the distinction between atomic flags and composite conveniences.
Name a combination when the domain uses it as a meaningful shorthand. Do not assign it a fresh bit unless it truly represents an independent state that can vary separately.
Zero deserves a deliberate meaning
The value with no bits set is valid and false:
empty = Permission(0)
print(bool(empty))
# FalseSometimes that is enough. In other domains, a named zero value makes intent clearer:
class Capability(Flag):
NONE = 0
READ = 1
WRITE = 2NONE does not represent another bit. It gives the empty set a domain name.
Use a named zero when “no capabilities,” “no options,” or a similar state appears directly in application logic or serialized data.
Avoid treating zero as an error automatically. For a flag set, zero naturally means that none of the independent options are enabled.
Removing a flag is not the same as toggling it
Developers sometimes use XOR to remove a flag:
permissions ^= Permission.WRITEThat only removes WRITE if it was already present. If it was absent, XOR adds it.
To ensure a flag is disabled, subtract the bit set conceptually by keeping everything except that flag:
permissions &= ~Permission.WRITETo ensure it is enabled:
permissions |= Permission.WRITEThese operations are idempotent: applying the same enable or disable operation repeatedly has the same final result.
That property is valuable in configuration and state-update code because callers do not need to know the previous state.
Use XOR only when “toggle” is genuinely the intended operation.
Flags are compact, but not always the clearest data model
A flag enum is a good fit when all of these are mostly true:
- options are independent and can coexist;
- the set of possible options is small and known;
- combining and testing options is common;
- one compact value is useful at an API or storage boundary;
- each option can be represented as present or absent.
Flags become less attractive when the state has richer structure.
For example, these values are not naturally flags:
timeout = 30 seconds
retry_count = 5
compression_level = 9They have values, not just presence.
Likewise, mutually exclusive choices often belong in a regular Enum:
from enum import Enum
class OutputFormat(Enum):
JSON = "json"
CSV = "csv"
XML = "xml"If exactly one format should be active, making each format a combinable bit would permit nonsensical states such as “JSON and CSV simultaneously.”
A dataclass or configuration object is often clearer when many fields have different types or validation rules.
The compactness of a bitmask is not automatically a design advantage. Use it when bitwise composition matches the domain semantics.
Common mistakes
Using consecutive integers for independent flags
This is wrong for atomic flags:
class BrokenPermission(Flag):
READ = 1
WRITE = 2
EXECUTE = 33 already contains the bits for 1 | 2, so EXECUTE is not independent.
Use powers of two or auto():
class Permission(Flag):
READ = auto()
WRITE = auto()
EXECUTE = auto()Using overlap when all flags are required
This test accepts any shared bit:
if available & required:
...Use containment when the entire requirement must be present:
if required in available:
...Choosing IntFlag without needing integers
IntFlag permits convenient interaction with numeric APIs, but that also makes accidental arithmetic and comparison with ordinary integers easier.
Prefer Flag for an internal domain model. Move to IntFlag when integer compatibility is a real boundary requirement.
Treating stored auto values as permanently stable
auto() expresses “assign an appropriate distinct value.” It is excellent for internal flags.
For persisted or protocol-visible masks, explicit numeric assignments make compatibility expectations reviewable and testable.
Assuming unknown external bits are harmless
An unknown bit may represent a future capability, malformed input, or a value with security meaning that the current program does not understand.
Choose whether to preserve, ignore, or reject unknown bits according to the boundary’s contract.
Conclusion
Flag and IntFlag are most useful when the domain really contains a set of independent yes-or-no options.
Give each atomic option one bit. Use OR to combine flags, AND to find overlap, membership to require complete containment, and complement-plus-AND to remove a flag reliably.
Prefer Flag when the value should stay inside a clear application type. Prefer IntFlag when numeric interoperability is part of an operating-system, protocol, persistence, or other external boundary.
Most importantly, do not choose flags merely because bitmasks are compact. Choose them when the bit-set mental model matches the problem. When it does, Python’s enum types turn low-level bit arithmetic into an API that communicates the domain directly.