Python Enum: Named Constants with the enum Module

By Engr. Henry Akpan

Stop Using Magic Strings and Numbers

Many programs have variables that can only take a few fixed values: an order is "pending", "paid" or "shipped"; a traffic light is red, amber or green; a grade is A to F. Storing these as plain strings or numbers invites bugs: a typo like "payed" is accepted silently, and nobody remembers what status 3 means. A Python enum (enumeration) defines a fixed set of named constants, so typos become errors, code becomes self-explaining, and editors can auto-complete the options. This tutorial covers creating enums with the enum module, members' names and values, iteration and lookup, auto(), comparing members, adding methods, IntEnum and StrEnum, Flag for combinable options, and using enums with the match statement. All outputs shown are from running the code.

Creating a Python Enum

Subclass Enum and list the members as class attributes:

from enum import Enum

class OrderStatus(Enum):
    PENDING = "pending"
    PAID = "paid"
    SHIPPED = "shipped"
    CANCELLED = "cancelled"

status = OrderStatus.PAID
print(status)
print(status.name, status.value)
print(type(status).__name__, isinstance(status, OrderStatus))

Output:

OrderStatus.PAID
PAID paid
OrderStatus True

Each member has a name (the identifier) and a value (what it was set to). By convention, member names are in capitals because they are constants.

Why Enums Catch Bugs

With plain strings, a typo is just another string. With an enum, it is an error the moment the code runs:

try:
    s = OrderStatus.PAYED
except AttributeError as e:
    print("AttributeError:", e)

try:
    s = OrderStatus("refunded")
except ValueError as e:
    print("ValueError:", e)

Output:

AttributeError: type object 'OrderStatus' has no attribute 'PAYED'
ValueError: 'refunded' is not a valid OrderStatus

Members also cannot be reassigned: OrderStatus.PAID = "x" raises an error, so the set of options can't be changed by accident.

Looking Up and Looping Over Members

You can get a member from its value with a call, or from its name with square brackets. This is how you convert data coming from a form, a database or JSON back into an enum:

from_db = "shipped"
print(OrderStatus(from_db))          # by value
print(OrderStatus["CANCELLED"])      # by name

for s in OrderStatus:
    print(s.name.title(), "->", s.value)
print(len(OrderStatus), [s.value for s in OrderStatus][:2])

Output:

OrderStatus.SHIPPED
OrderStatus.CANCELLED
Pending -> pending
Paid -> paid
Shipped -> shipped
Cancelled -> cancelled
4 ['pending', 'paid']

Members are listed in the order they were defined, which makes enums good for building drop-down menus.

Comparing Members

Compare enum members with is or ==. Each member is a single object (a singleton), so is is safe and common. A plain Enum member is not equal to its raw value:

status = OrderStatus.PAID
print(status is OrderStatus.PAID, status == OrderStatus.PAID)
print(status == "paid", status.value == "paid")

Output:

True True
False True

This strictness is deliberate: it stops you mixing up enum members with arbitrary strings. Compare .value when you really mean the raw value, or use StrEnum (below).

auto() and Unique Values

When the actual values don't matter, auto() numbers the members for you. The @unique decorator raises an error if two members share a value (otherwise the second becomes an alias of the first):

from enum import auto, unique

@unique
class Level(Enum):
    BEGINNER = auto()
    INTERMEDIATE = auto()
    ADVANCED = auto()

print([(m.name, m.value) for m in Level])

Output:

[('BEGINNER', 1), ('INTERMEDIATE', 2), ('ADVANCED', 3)]

Adding Methods and Properties

Enums are classes, so they can carry behaviour. Here each grade knows its points on the Nigerian 5-point scale and whether it is a pass:

class Grade(Enum):
    A = 5
    B = 4
    C = 3
    D = 2
    E = 1
    F = 0

    @property
    def points(self):
        return self.value

    def is_pass(self):
        return self is not Grade.F

    @classmethod
    def from_score(cls, score):
        for limit, grade in [(70, cls.A), (60, cls.B), (50, cls.C), (45, cls.D), (40, cls.E)]:
            if score >= limit:
                return grade
        return cls.F

for score in [82, 55, 38]:
    g = Grade.from_score(score)
    print(score, g.name, g.points, g.is_pass())

Output:

82 A 5 True
55 C 3 True
38 F 0 False

Putting the logic inside the enum keeps all grade rules in one place instead of scattering if statements across the program.

IntEnum and StrEnum

Sometimes you need members that behave like ordinary numbers or strings, for example when sending them to an API or comparing with existing data. IntEnum members are also integers, and StrEnum (Python 3.11+) members are also strings:

from enum import IntEnum, StrEnum

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

class Colour(StrEnum):
    RED = "red"
    GREEN = "green"

print(Priority.HIGH > Priority.LOW, Priority.MEDIUM + 1)
print([p.name for p in sorted([Priority.HIGH, Priority.LOW, Priority.MEDIUM])])
print(Colour.RED == "red", Colour.GREEN.upper(), f"{Colour.RED}")

Output:

True 3
['LOW', 'MEDIUM', 'HIGH']
True GREEN red

Use these only when you need the mixing; plain Enum gives stronger protection against accidental comparisons.

Flag: Combining Options

A Flag enum lets members be combined with |, which is ideal for permissions or settings where several can be on at once:

from enum import Flag

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

editor = Permission.READ | Permission.WRITE
print(Permission.WRITE in editor, Permission.DELETE in editor)
print([p.name for p in Permission if p in editor])
print(Permission.READ.value, Permission.WRITE.value, Permission.DELETE.value)

Output:

True False
['READ', 'WRITE']
1 2 4

auto() gives a Flag powers of two (1, 2, 4, …), so each option is a separate bit, exactly like file permissions in Linux.

Enums with the match Statement

Python's match statement (3.10+) works neatly with enum members:

def next_step(status):
    match status:
        case OrderStatus.PENDING:
            return "Wait for payment"
        case OrderStatus.PAID:
            return "Pack and ship"
        case OrderStatus.SHIPPED:
            return "Track delivery"
        case _:
            return "No action"

for s in OrderStatus:
    print(s.value, "=>", next_step(s))

Output:

pending => Wait for payment
paid => Pack and ship
shipped => Track delivery
cancelled => No action

Dotted names like OrderStatus.PAID are treated as values to compare against, not as new variables to capture, which is exactly what we want here.

Storing Enums in JSON or a Database

JSON and databases don't understand enum objects, so store member.value (or member.name) and convert back with OrderStatus(value) when loading. Storing the value keeps the data readable and survives renaming a member in code, as long as the value stays the same.

Summary

  • A Python enum defines a fixed set of named constants; each member has a name and a value.
  • Typos and invalid values raise errors immediately instead of silently creating bad data.
  • Look up members with Enum(value) or Enum["NAME"]; loop over them in definition order.
  • Compare members with is; plain enum members are not equal to raw strings or numbers.
  • auto(), @unique, methods, IntEnum, StrEnum and Flag cover most real needs.

Related Tutorials