Python Enum: The Complete Guide to Enumerations

Magic numbers and loose string constants lurking in your code create bugs that are hard to trace and even harder to maintain. Python's enum module solves this by letting you define fixed sets of named constants that are readable, type-safe, and impossible to accidentally reassign. This guide covers everything from basic Enum creation to advanced patterns with Flag, StrEnum, and the verify decorator.

The enum module has been part of Python's standard library since version 3.4, and it has continued to evolve through subsequent releases. Python 3.11 introduced StrEnum, ReprEnum, and the verify decorator. Python 3.13 refined how auto() calculates next values and added the _member_names_ attribute. Whether you are writing a small script or architecting a large application, enumerations bring structure, clarity, and safety to any codebase that relies on fixed sets of values.

What Is an Enum and Why Use One

An enumeration is a set of symbolic names, called members, that are bound to unique, constant values. Think of it as a way to give meaningful labels to values that would otherwise be anonymous integers or scattered strings. Instead of writing status = 1 somewhere and hoping everyone remembers that 1 means "pending," you write status = OrderStatus.PENDING and the intent becomes self-documenting.

Enumerations provide several advantages over plain constants. Members are immutable, which means they cannot be reassigned or modified after the class is created. They are singletons, so comparing two references to the same member with is always returns True. They support iteration, letting you loop over every defined value in the enum. And they produce clear, readable output when printed or logged, because repr() and str() include the class name and member name by default.

Note

Enum members evaluate to True in boolean contexts, even if their value is 0. This is different from regular integers and is an intentional design decision. If you need integer-like truthiness behavior, use IntEnum instead.

Creating Your First Enum

Creating an enum starts with importing the Enum class and defining a new class that inherits from it. Each class attribute you assign becomes a member of the enumeration.

from enum import Enum

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

Once defined, you access members as attributes of the class. Every member has two core properties: name, which is the string name you gave it, and value, which is whatever you assigned to it.

# Access a member
print(Color.RED)         # Color.RED
print(Color.RED.name)    # RED
print(Color.RED.value)   # 1

# Look up by value
print(Color(2))          # Color.GREEN

# Look up by name
print(Color["BLUE"])     # Color.BLUE

Enums also support iteration and membership testing, which makes them convenient for validation logic and building user-facing menus or dropdowns.

# Iterate over all members
for color in Color:
    print(f"{color.name} = {color.value}")

# Membership testing
print(Color.RED in Color)  # True

The Functional API

If you prefer a more compact syntax, Python lets you create enums using a functional call. This is useful when you are generating enums dynamically or want a quick one-liner.

Color = Enum("Color", ["RED", "GREEN", "BLUE"])

# Values are automatically assigned starting at 1
print(Color.RED.value)    # 1
print(Color.GREEN.value)  # 2
Pro Tip

The convention is to use UPPER_CASE names for enum members. This makes them visually distinct from regular class attributes and methods, and aligns with how constants are typically named in Python.

auto() and Controlling Member Values

Manually assigning a value to every member can become tedious, especially when the specific values do not matter. The auto() function handles this by generating values for you automatically.

from enum import Enum, auto

class Priority(Enum):
    LOW = auto()
    MEDIUM = auto()
    HIGH = auto()
    CRITICAL = auto()

print(Priority.LOW.value)       # 1
print(Priority.CRITICAL.value)  # 4

By default, auto() starts at 1 and increments by 1 for each subsequent member. The reason it starts at 1 rather than 0 is that 0 evaluates to False in a boolean context, and enum members are designed to always be truthy.

Customizing auto() with _generate_next_value_

You can override the _generate_next_value_ static method to change how auto() produces values. A common pattern is to use the lowercased member name as the value instead of an integer.

class HttpMethod(Enum):
    @staticmethod
    def _generate_next_value_(name, start, count, last_values):
        return name.lower()

    GET = auto()
    POST = auto()
    PUT = auto()
    DELETE = auto()

print(HttpMethod.GET.value)     # get
print(HttpMethod.DELETE.value)  # delete
Note

Starting in Python 3.13, auto() always uses the highest existing member value incremented by 1, rather than the last-seen value. This makes behavior more predictable when members are defined out of numeric order. If any member has an incompatible type, auto() will raise an error.

IntEnum, StrEnum, and Other Specialized Types

The base Enum class keeps its members isolated from other types. You cannot compare a Color.RED member to the integer 1 and get True. This is a feature, not a limitation -- it prevents accidental comparisons that would be meaningless. However, there are legitimate cases where you want enum members to behave like their underlying primitive type.

IntEnum

IntEnum members are full subclasses of int. They can be used in arithmetic, comparisons with plain integers, and anywhere an integer is expected.

from enum import IntEnum

class HttpStatus(IntEnum):
    OK = 200
    NOT_FOUND = 404
    SERVER_ERROR = 500

# Behaves like an integer
print(HttpStatus.OK == 200)          # True
print(HttpStatus.NOT_FOUND + 1)      # 405
print(HttpStatus.OK < HttpStatus.NOT_FOUND)  # True
Warning

When you perform arithmetic on an IntEnum member, the result loses its enumeration status and becomes a plain int. For example, HttpStatus.OK + 1 returns 201, not a member of HttpStatus. Use IntEnum only when you genuinely need integer compatibility.

StrEnum

StrEnum was introduced in Python 3.11 and works just like IntEnum but for strings. Members are both enum members and str subclasses, so they work seamlessly in string operations.

from enum import StrEnum

class ContentType(StrEnum):
    JSON = "application/json"
    HTML = "text/html"
    PLAIN = "text/plain"

# Use directly in string contexts
headers = {"Content-Type": ContentType.JSON}
print(headers)  # {'Content-Type': 'application/json'}

# String comparisons work
print(ContentType.JSON == "application/json")  # True

When you use auto() with StrEnum, the generated value is the lowercased version of the member name, rather than an incrementing integer.

class Env(StrEnum):
    DEVELOPMENT = auto()
    STAGING = auto()
    PRODUCTION = auto()

print(Env.DEVELOPMENT.value)  # development
print(Env.PRODUCTION.value)   # production

Flag and IntFlag for Bitwise Operations

When you need to combine multiple options into a single value using bitwise operators, Flag and IntFlag are the right tools. Each member represents a single bit, and you can combine them with | (OR), & (AND), and ~ (NOT).

from enum import Flag, auto

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

# Combine permissions
user_perms = Permission.READ | Permission.WRITE
print(user_perms)                       # Permission.READ|WRITE
print(Permission.READ in user_perms)    # True
print(Permission.EXECUTE in user_perms) # False

With Flag, auto() assigns powers of two (1, 2, 4, 8, ...) rather than sequential integers. This ensures each member occupies a single bit in the combined value.

You can also predefine common combinations as members of the same enum.

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

    # Composite members
    READ_WRITE = READ | WRITE
    ALL = READ | WRITE | EXECUTE

admin_perms = Permission.ALL
print(Permission.WRITE in admin_perms)  # True

IntFlag works the same way but also inherits from int, so combined values can be used in integer contexts. Choose Flag when you want strict type safety and IntFlag when you need backward compatibility with integer-based APIs.

Aliases, @unique, and the verify Decorator

By default, Python allows multiple names to share the same value. When this happens, the first name defined becomes the canonical member and any subsequent names with the same value become aliases.

class Shape(Enum):
    SQUARE = 2
    DIAMOND = 1
    CIRCLE = 3
    ALIAS_FOR_SQUARE = 2  # This is an alias

print(Shape.ALIAS_FOR_SQUARE is Shape.SQUARE)  # True

# Aliases are excluded from iteration
print(list(Shape))
# [<Shape.SQUARE: 2>, <Shape.DIAMOND: 1>, <Shape.CIRCLE: 3>]

If you want to prevent aliases entirely, use the @unique decorator. It raises a ValueError at class creation time if any two members share a value.

from enum import Enum, unique

@unique
class Direction(Enum):
    NORTH = 1
    SOUTH = 2
    EAST = 3
    WEST = 4
    # UP = 1  <-- would raise ValueError

The verify Decorator

Python 3.11 introduced the verify decorator, which provides more flexible validation than @unique alone. It accepts one or more check options from the EnumCheck enum.

from enum import Enum, verify, UNIQUE, CONTINUOUS, NAMED_FLAGS

@verify(UNIQUE)
class Season(Enum):
    SPRING = 1
    SUMMER = 2
    AUTUMN = 3
    WINTER = 4

@verify(CONTINUOUS)
class Grade(Enum):
    A = 1
    B = 2
    C = 3
    # D = 5  <-- would raise ValueError: missing value 4

UNIQUE enforces the same constraint as the @unique decorator. CONTINUOUS ensures there are no gaps between the lowest and highest integer values. NAMED_FLAGS ensures that any composite flags are built only from individually named members.

Advanced Patterns and Custom Methods

Enum classes are full Python classes, which means you can add methods, properties, and even custom __init__ logic to them.

Adding Methods to Enums

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

    def __init__(self, mass, radius):
        self.mass = mass       # in kilograms
        self.radius = radius   # in meters

    @property
    def surface_gravity(self):
        G = 6.67300e-11  # universal gravitational constant
        return G * self.mass / (self.radius * self.radius)

print(Planet.EARTH.surface_gravity)  # 9.802652743337129
print(Planet.MARS.mass)              # 6.421e+23

This pattern is powerful for building domain-specific enumerations where each member carries structured data along with its identity.

Using Mixins

You can mix in other types by placing them before Enum in the class definition. This gives members the behavior of both the mixin type and the enum.

class OrderedStrEnum(str, Enum):
    """Members are comparable strings with ordering."""
    def __lt__(self, other):
        if self.__class__ is other.__class__:
            members = list(self.__class__)
            return members.index(self) < members.index(other)
        return NotImplemented

class Size(OrderedStrEnum):
    SMALL = "S"
    MEDIUM = "M"
    LARGE = "L"
    XLARGE = "XL"

print(Size.SMALL < Size.LARGE)     # True
print(Size.MEDIUM == "M")          # True

The _missing_ Hook

Override _missing_ to intercept value lookups that do not match any member. This is useful for case-insensitive matching or mapping legacy values.

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

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

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

Real-World Use Cases

Enumerations are not just an academic exercise. They appear throughout production Python code in patterns like these.

State Machines

Enums are a natural fit for representing states and transitions. Defining allowed states as enum members means invalid states become impossible to construct.

class OrderStatus(Enum):
    PENDING = auto()
    CONFIRMED = auto()
    SHIPPED = auto()
    DELIVERED = auto()
    CANCELLED = auto()

    def can_transition_to(self, new_status):
        transitions = {
            OrderStatus.PENDING: {OrderStatus.CONFIRMED, OrderStatus.CANCELLED},
            OrderStatus.CONFIRMED: {OrderStatus.SHIPPED, OrderStatus.CANCELLED},
            OrderStatus.SHIPPED: {OrderStatus.DELIVERED},
            OrderStatus.DELIVERED: set(),
            OrderStatus.CANCELLED: set(),
        }
        return new_status in transitions[self]

order = OrderStatus.PENDING
print(order.can_transition_to(OrderStatus.CONFIRMED))  # True
print(order.can_transition_to(OrderStatus.DELIVERED))   # False

Configuration and Feature Flags

class Feature(Flag):
    DARK_MODE = auto()
    NOTIFICATIONS = auto()
    BETA_FEATURES = auto()
    ADMIN_PANEL = auto()

    DEFAULT = DARK_MODE | NOTIFICATIONS

user_features = Feature.DEFAULT | Feature.BETA_FEATURES
print(Feature.DARK_MODE in user_features)     # True
print(Feature.ADMIN_PANEL in user_features)   # False

Database and API Integration

StrEnum members work directly as string values in dictionaries, JSON payloads, and database queries without requiring any manual .value access.

import json

class Role(StrEnum):
    ADMIN = "admin"
    EDITOR = "editor"
    VIEWER = "viewer"

# Serializes cleanly to JSON
payload = {"user": "alice", "role": Role.ADMIN}
print(json.dumps(payload))
# {"user": "alice", "role": "admin"}

Key Takeaways

  1. Use Enum for type-safe constants: Whenever you find yourself defining a group of related constants as plain integers or strings, reach for Enum instead. You get immutability, identity-based comparison, iteration, and clear repr() output for free.
  2. Pick the right subclass for interoperability: Use IntEnum when you need integer compatibility, StrEnum when you need string compatibility, and Flag/IntFlag when you need to combine members with bitwise operators. Use the base Enum class when you want strict isolation from other types.
  3. Leverage auto() and verify for cleaner code: Let auto() handle value assignment when specific values do not matter. Use the @verify decorator with UNIQUE, CONTINUOUS, or NAMED_FLAGS to catch structural problems at class creation time rather than at runtime.
  4. Add behavior to your enums: Enum classes support custom methods, properties, __init__ with tuple unpacking, and hooks like _missing_. This turns them into rich domain objects rather than flat lookups, making them powerful tools for patterns like state machines and configuration management.

Python's enum module transforms what would otherwise be fragile, scattered constants into structured, self-documenting types. Start using enumerations wherever you have a fixed set of values, and you will find that bugs decrease, code reviews go faster, and your intent becomes clearer for everyone who reads your code.

back to articles