W3docs

Python Enums

Learn Python enums: create Enum, IntEnum, Flag, and auto() members, add methods, compare safely, and replace fragile magic numbers in your code.

An enum (short for enumeration) is a set of named, constant values grouped under a single type. Instead of scattering bare integers or strings like 1, 2, "pending", "active" through your code, you give each one a descriptive name — Status.PENDING, Color.RED — and Python guarantees that name always maps to the same value.

This chapter covers:

  • Why enums exist and what problems they solve
  • Creating an enum with the Enum class
  • Accessing members by name and by value
  • Iterating over an enum
  • auto() — letting Python assign values for you
  • IntEnum — enums that behave like integers
  • Flag — combinable bit-flag enums
  • Adding methods and properties to an enum
  • Aliases, @unique, and _missing_
  • When to use enums vs. other patterns

Before reading this chapter, make sure you are comfortable with Python classes and objects and Python data types.

Why Use Enums?

Consider this function that processes an order status passed as a plain integer:

def handle_order(status):
    if status == 1:
        print("Order is pending")
    elif status == 2:
        print("Order is active")
    elif status == 3:
        print("Order is complete")

This works but has real problems:

  • Magic numbers. What does 2 mean in isolation? You have to trace back to the function definition.
  • No validation. handle_order(99) silently does nothing — no error, no warning.
  • Typos are invisible. handle_order(2) and handle_order(20) are both valid Python.
  • Refactoring is risky. If you decide 1 should mean something else, you have to find every 1 in the codebase.

Enums fix all of these. The same logic written with an enum is self-documenting, safe, and refactor-friendly:

from enum import Enum

class OrderStatus(Enum):
    PENDING = 1
    ACTIVE = 2
    COMPLETE = 3

def handle_order(status: OrderStatus):
    if status == OrderStatus.PENDING:
        print("Order is pending")
    elif status == OrderStatus.ACTIVE:
        print("Order is active")
    elif status == OrderStatus.COMPLETE:
        print("Order is complete")

handle_order(OrderStatus.ACTIVE)   # Order is active

The intent is clear, and Python prevents handle_order(99) from accidentally matching any branch.

Creating an Enum

Import Enum from the enum module (part of the Python standard library — no installation needed) and subclass it:

from enum import Enum

class Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3

Each class-level attribute (RED, GREEN, BLUE) becomes an enum member. The values on the right (1, 2, 3) can be integers, strings, or any other type — the choice is up to you.

Accessing members

There are three ways to reach an enum member:

from enum import Enum

class Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3

# Attribute access (most common)
print(Color.RED)           # Color.RED

# By name (square bracket notation)
print(Color['GREEN'])      # Color.GREEN

# By value (call the class with the value)
print(Color(3))            # Color.BLUE

Every member exposes two attributes:

print(Color.RED.name)      # RED
print(Color.RED.value)     # 1

Use .name when you need a human-readable label (for logging or display), and .value when you need to pass the underlying value to an external system (a database, an API).

repr and type

print(repr(Color.RED))     # <Color.RED: 1>
print(type(Color.RED))     # <enum 'Color'>

An enum member is an instance of its enum class, not of int or str.

Iterating Over an Enum

Enums are iterable. Iteration yields members in definition order:

from enum import Enum

class Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3

for color in Color:
    print(color.name, color.value)
# RED 1
# GREEN 2
# BLUE 3

You can also check membership:

print(Color.RED in Color)   # True

This makes enums handy for populating drop-downs, building switch-like dispatch tables, or building lists of choices for user input.

auto() — Automatic Values

If the specific values do not matter — you only care that each member is distinct — use auto(). Python assigns sequential integers starting from 1:

from enum import Enum, auto

class Direction(Enum):
    NORTH = auto()
    SOUTH = auto()
    EAST = auto()
    WEST = auto()

for d in Direction:
    print(d.name, d.value)
# NORTH 1
# SOUTH 2
# EAST 3
# WEST 4

auto() is especially useful when the enum will grow over time and you do not want to manually renumber members.

Comparing Enum Members

Use is or == to compare members. Both work, but is is slightly faster because enum members are singletons — each name maps to exactly one object:

from enum import Enum

class Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3

print(Color.RED is Color.RED)    # True
print(Color.RED == Color.RED)    # True
print(Color.RED == Color.GREEN)  # False

A plain Enum member does not equal its raw value:

print(Color.RED == 1)   # False

This is intentional. It prevents accidental equality between different enums that share the same integer:

class Size(Enum):
    SMALL = 1

print(Color.RED == Size.SMALL)   # False — different types

If you need value-based comparison (e.g., member > 1), use IntEnum instead (see below).

IntEnum — Enums That Behave Like Integers

IntEnum members are also regular Python integers. This means you can use arithmetic, comparison operators, and pass them anywhere an int is expected:

from enum import IntEnum

class Priority(IntEnum):
    LOW = 1
    MEDIUM = 2
    HIGH = 3

print(Priority.HIGH > Priority.LOW)    # True
print(Priority.MEDIUM + 10)            # 12
print(Priority.HIGH == 3)              # True

A common use case is sorting a list of enum members:

from enum import IntEnum

class Level(IntEnum):
    LOW = 1
    MED = 2
    HIGH = 3

levels = [Level.HIGH, Level.LOW, Level.MED]
print([l.name for l in sorted(levels)])   # ['LOW', 'MED', 'HIGH']

When to prefer Enum over IntEnum

IntEnum's integer transparency is also its weakness: Priority.HIGH == 3 is True, so a mistyped literal 3 will silently compare equal to Priority.HIGH. Use plain Enum whenever you want strict type safety, and use IntEnum only when you genuinely need integer arithmetic or must interface with an API that deals in raw numbers.

Flag — Combinable Bit-Flag Enums

Flag is designed for scenarios where multiple options can be active at the same time. Its members are powers of two, and you combine them with the | (bitwise OR) operator:

from enum import Flag, auto

class Permission(Flag):
    READ = auto()
    WRITE = auto()
    EXECUTE = auto()
    ALL = READ | WRITE | EXECUTE

user = Permission.READ | Permission.WRITE
print(user)                           # Permission.WRITE|READ
print(Permission.READ in user)        # True
print(Permission.EXECUTE in user)     # False

auto() inside Flag assigns successive powers of two (1, 2, 4, 8, …) so that combining members with | never produces ambiguous results.

Use Flag for permission systems, feature toggles, and any situation where you need a compact set of boolean switches.

Adding Methods and Properties

Because an enum is a class, you can add methods and properties to it. This keeps related logic inside the type rather than scattered across if/elif chains:

from enum import Enum

class HttpStatus(Enum):
    OK = 200
    CREATED = 201
    NOT_FOUND = 404
    INTERNAL_ERROR = 500

    @property
    def is_success(self):
        return 200 <= self.value < 300

    @property
    def is_error(self):
        return self.value >= 400

def handle_response(status: HttpStatus):
    if status.is_success:
        print(f"{status.value} {status.name}: request succeeded")
    elif status.is_error:
        print(f"{status.value} {status.name}: request failed")

handle_response(HttpStatus.OK)           # 200 OK: request succeeded
handle_response(HttpStatus.NOT_FOUND)    # 404 NOT_FOUND: request failed

You can also give an enum a custom __init__ to store extra data per member. Provide the values as tuples:

from enum import Enum

class Planet(Enum):
    MERCURY = (3.303e+23, 2.4397e6)
    VENUS   = (4.869e+24, 6.0518e6)
    EARTH   = (5.976e+24, 6.37814e6)

    def __init__(self, mass, radius):
        self.mass = mass
        self.radius = radius

    @property
    def surface_gravity(self):
        G = 6.67430e-11
        return G * self.mass / (self.radius ** 2)

print(round(Planet.EARTH.surface_gravity, 2))    # 9.8
print(round(Planet.MERCURY.surface_gravity, 2))  # 3.7

The (mass, radius) tuple becomes the constructor arguments; self.value still holds the full tuple.

Aliases and @unique

If two members share the same value, the second becomes an alias — it resolves to the first member. Aliases are not yielded during iteration:

from enum import Enum

class Status(Enum):
    ACTIVE = 1
    RUNNING = 1   # alias for ACTIVE

print(Status.ACTIVE is Status.RUNNING)   # True
print(list(Status))                      # [<Status.ACTIVE: 1>]

Aliases are occasionally useful (e.g., a legacy name pointing to a new one), but they can also mask typos. Apply the @unique decorator to forbid duplicate values entirely:

from enum import Enum, unique

@unique
class Status(Enum):
    PENDING = 1
    ACTIVE = 2
    INACTIVE = 3

# Trying to add a duplicate value to a @unique enum raises ValueError:
# ValueError: duplicate values found in <enum 'Bad'>: B -> A

@unique is a good default for any enum where accidental aliasing would be a bug.

Custom Lookup with _missing_

By default, calling Color('unknown') raises a ValueError. You can override the class method _missing_ to handle unrecognised values — for example, to do a case-insensitive lookup:

from enum import Enum

class Color(Enum):
    RED = 'red'
    GREEN = 'green'
    BLUE = 'blue'

    @classmethod
    def _missing_(cls, value):
        if isinstance(value, str):
            for member in cls:
                if member.value == value.lower():
                    return member
        return None

print(Color('RED'))     # Color.RED
print(Color('Green'))   # Color.GREEN

_missing_ receives the value that was not found. Return the matching member, or None (which lets Python raise its default ValueError).

When to Use Enums

Enums are the right choice when:

  • A variable can only hold one of a fixed set of named states (order status, HTTP verb, card suit).
  • You want to prevent invalid values from silently passing through.
  • The same concept is compared in multiple places and you want a single source of truth.
  • You need to iterate over all valid values (populating a form, documenting an API).

You probably do not need an enum when:

  • The set of values is open-ended or changes at runtime (use a dictionary or a database lookup table).
  • You only need two statesTrue/False with a clear boolean meaning is simpler.
  • The values come from user input that must be validated against a schema — consider a library like Pydantic, which integrates neatly with Python enums.

For closely related patterns, see Python dataclasses (for structured data with default values) and Python abstract classes (for enforcing interface contracts across subclasses). If you need named constant containers without the full enum machinery, the Python collections module offers namedtuple as an alternative.

Practice

Practice
What does Color['RED'] do when Color is an Enum with a RED member?
What does Color['RED'] do when Color is an Enum with a RED member?
Was this page helpful?