Creating an Enum in Python: Complete Enumeration Type Definition and Usage Implementation Guide

Creating an enum in python

Before I discovered Python’s enum module, I represented fixed sets of options using plain strings or integer constants scattered across my code — status = "pending", status = "active", and so on. It worked until typos crept in ("pendign" anyone?) and I had no way to enforce that a variable could only hold one of a specific set of values. Switching to Enum fixed that entirely, and it’s now one of the first things I reach for when modeling a fixed set of related constants. This guide covers everything from the basics of defining an enum to how it works internally and where it genuinely improves real code.

What Is an Enum

An enumeration (Enum) is a set of symbolic names bound to unique, constant values. Instead of scattering string or integer literals throughout code, an enum groups related constants under a single, well-defined type.

from enum import Enum

class Status(Enum):
    PENDING = 1
    ACTIVE = 2
    COMPLETED = 3
    CANCELLED = 4

print(Status.ACTIVE)         # Status.ACTIVE
print(Status.ACTIVE.name)    # 'ACTIVE'
print(Status.ACTIVE.value)   # 2

Why I Use Enums Instead of Plain Constants

Before enums, this was common in my code:

STATUS_PENDING = "pending"
STATUS_ACTIVE = "active"

def update_status(order, status):
    if status not in ("pending", "active", "completed"):
        raise ValueError("Invalid status")
    order.status = status

With enums, invalid values become structurally impossible rather than something I have to manually validate every time:

from enum import Enum

class Status(Enum):
    PENDING = "pending"
    ACTIVE = "active"
    COMPLETED = "completed"

def update_status(order, status: Status):
    order.status = status  # status is guaranteed to be a valid Status member

Type checkers like mypy can also catch mismatches at development time, something plain strings never allowed.

Accessing Enum Members

There are multiple ways to access enum members, each useful in different contexts:

class Status(Enum):
    PENDING = 1
    ACTIVE = 2

print(Status.PENDING)          # By attribute access
print(Status["PENDING"])       # By name (string) lookup
print(Status(1))               # By value lookup

for status in Status:
    print(status.name, status.value)

Value lookup (Status(1)) is especially useful when deserializing data from a database or API where I only have the raw value, not the name.

raw_value_from_db = 2
status = Status(raw_value_from_db)
print(status)  # Status.ACTIVE

Comparing Enum Members

Enum members compare by identity, not just equality, and are automatically singleton instances:

print(Status.ACTIVE == Status.ACTIVE)  # True
print(Status.ACTIVE is Status.ACTIVE)  # True
print(Status.ACTIVE == Status.PENDING) # False
print(Status.ACTIVE == 2)               # False, plain Enum doesn't equal its raw value

That last line surprises people coming from other languages — a plain Enum member is not automatically equal to its underlying value. If I need that behavior, I use IntEnum or StrEnum instead (covered below).

Enum Variants: IntEnum, StrEnum, Flag, IntFlag

Python’s enum module provides several specialized base classes beyond plain Enum.

IntEnum

Members behave like actual integers, useful when interoperating with code expecting raw numbers:

from enum import IntEnum

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

print(Priority.HIGH == 3)     # True
print(Priority.HIGH > Priority.LOW)  # True
print(Priority.HIGH + 1)      # 4, behaves like an int

StrEnum (Python 3.11+)

Members behave like actual strings:

from enum import StrEnum

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

print(Color.RED == "red")   # True
print(f"Selected: {Color.RED}")  # Selected: red

For earlier Python versions, I achieve the same effect by inheriting from both str and Enum:

class Color(str, Enum):
    RED = "red"
    GREEN = "green"

Flag and IntFlag

For combinable bitmask-style options, covered in more depth in bitwise-operator contexts:

from enum import Flag, auto

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

combined = Permission.READ | Permission.WRITE
print(combined)                      # Permission.READ|WRITE
print(Permission.READ in combined)   # True

Using auto() for Automatic Values

When the actual numeric or string value doesn’t matter, auto() saves me from manually assigning them:

from enum import Enum, auto

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

print(Direction.NORTH.value)  # 1
print(Direction.WEST.value)   # 4

By default auto() generates sequential integers starting at 1, but this behavior can be customized by overriding _generate_next_value_.

Adding Methods and Properties to Enums

Enums are real classes, which means I can attach methods, properties, and even custom __init__ logic:

from enum import Enum

class Planet(Enum):
    MERCURY = (3.3e23, 2.4e6)
    VENUS = (4.9e24, 6.1e6)
    EARTH = (5.9e24, 6.4e6)

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

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

print(Planet.EARTH.surface_gravity())  # ~9.8

This pattern — attaching computed behavior directly to enum members — is one of my favorite features, since it keeps related data and logic together instead of scattered across lookup dictionaries.

Enums With Aliases

Multiple names can point to the same value, creating aliases. By default, Python treats the second name as an alias for the first, not a distinct member:

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

print(Status.ACTIVE is Status.ENABLED)  # True
print(list(Status))  # [<Status.ACTIVE: 1>] — ENABLED doesn't appear separately

If I want to explicitly disallow accidental aliasing, I use the @unique decorator:

from enum import Enum, unique

@unique
class Status(Enum):
    ACTIVE = 1
    ENABLED = 1  # Raises ValueError: duplicate values found

How Enums Work Internally

Under the hood, Enum uses a metaclass (EnumMeta/EnumType) that intercepts class creation to transform simple class attributes into Enum member instances, ensures each member is a singleton, and wires up the lookup machinery (Status["NAME"], Status(value)) automatically.

Each enum member is created exactly once and cached, which is why identity comparison (is) works reliably — unlike regular class instances, calling Status(1) twice returns the same object both times rather than creating a new one.

print(Status(1) is Status(1))  # True

This singleton behavior also means enum members are memory-efficient regardless of how many times they’re referenced throughout a codebase.

Real-World Use Cases

API status codes:

class HTTPStatus(Enum):
    OK = 200
    NOT_FOUND = 404
    SERVER_ERROR = 500

def handle_response(code):
    status = HTTPStatus(code)
    if status == HTTPStatus.OK:
        print("Success")
    elif status == HTTPStatus.NOT_FOUND:
        print("Resource missing")

State machines:

class OrderState(Enum):
    CREATED = auto()
    PAID = auto()
    SHIPPED = auto()
    DELIVERED = auto()

VALID_TRANSITIONS = {
    OrderState.CREATED: {OrderState.PAID},
    OrderState.PAID: {OrderState.SHIPPED},
    OrderState.SHIPPED: {OrderState.DELIVERED},
}

def transition(current, target):
    if target not in VALID_TRANSITIONS.get(current, set()):
        raise ValueError(f"Cannot go from {current} to {target}")
    return target

Configuration options in automation scripts:

class LogLevel(StrEnum):
    DEBUG = "debug"
    INFO = "info"
    WARNING = "warning"
    ERROR = "error"

def configure_logging(level: LogLevel):
    print(f"Logging configured at {level} level")

Best Practices

  • Use Enum for general symbolic constants, IntEnum/StrEnum only when interoperability with raw ints/strings is genuinely needed.
  • Use auto() when the specific underlying value doesn’t matter, to reduce manual bookkeeping and merge conflicts.
  • Use @unique when accidental aliasing would be a bug rather than an intentional feature.
  • Attach behavior directly to enum members via methods rather than external lookup dictionaries, when that behavior is intrinsic to the member.

Common Mistakes

# Mistake: assuming plain Enum members equal their raw values
class Status(Enum):
    ACTIVE = 1

print(Status.ACTIVE == 1)  # False! Use IntEnum if this comparison is needed

# Mistake: expecting duplicate values to create distinct members
class Bad(Enum):
    A = 1
    B = 1

print(list(Bad))  # Only [<Bad.A: 1>], B is an alias

Debugging Tips

  • Use list(MyEnum) to see all actual (non-alias) members.
  • Use MyEnum.__members__ to see all names, including aliases, mapped to their members.
  • When a lookup fails, check whether you’re passing a value (MyEnum(value)) versus a name (MyEnum["NAME"]) — mixing these up is a very common ValueError/KeyError source.

FAQs

What’s the difference between Enum and IntEnum? Plain Enum members don’t compare equal to their raw underlying value; IntEnum members behave like actual integers and do compare equal to matching int values.

Can enum values be non-numeric? Yes — enum values can be strings, tuples, or any hashable value, not just integers.

How do I iterate over all enum members? Use for member in MyEnum: — this iterates in definition order and skips aliases.

Are enum members singletons? Yes — calling MyEnum(value) or MyEnum["NAME"] multiple times always returns the exact same object, enabling reliable is comparisons.

Summary

Python’s enum module turns loose collections of related constants into a proper, type-safe construct. Between plain Enum, IntEnum, StrEnum, and Flag/IntFlag, there’s a variant suited to nearly every use case I’ve run into — status codes, state machines, configuration options, and bitmask-style flags. Since adopting enums over raw string/int constants, entire categories of typo-driven bugs have simply stopped happening in my code.

References

Total
0
Shares

Leave a Reply

Previous Post
Parsing string to datetime object in python

Parsing String to Datetime Object in Python: Complete strptime() and Date Parsing Implementation Guide

Next Post
Operations on sets in python

Operations on Sets in Python: Complete Set Theory Operations, Union, Intersection, and Difference Guide

Related Posts