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  100

No two options share a bit. Combining READ and WRITE with bitwise OR produces:

READ     001
WRITE    010
         ---
OR       011

The 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.WRITE

read_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.EXECUTE

After 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.WRITE

writable is Permission.WRITE.

If there is no overlap, the result is the zero-valued flag:

overlap = Permission.READ & Permission.EXECUTE

print(bool(overlap))
# False

A 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.WRITE

The 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 = ~permissions

With 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)
# False

That 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 permissions

The 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 = 4

You can combine members and convert the result to an integer:

permissions = WirePermission.READ | WirePermission.WRITE

encoded = int(permissions)
print(encoded)
# 3

And you can construct the flag value again from that integer:

decoded = WirePermission(3)

print(WirePermission.READ in decoded)
# True

This 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)
# 3

The 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 = 4

Now 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)
# 8

This 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 | WRITE

Now both expressions describe the same value:

Permission.READ_WRITE == Permission.READ | Permission.WRITE
# True

This 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))
# False

Sometimes that is enough. In other domains, a named zero value makes intent clearer:

class Capability(Flag):
    NONE = 0
    READ = 1
    WRITE = 2

NONE 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.WRITE

That 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.WRITE

To ensure it is enabled:

permissions |= Permission.WRITE

These 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 = 9

They 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 = 3

3 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.