Maria runs a small Etsy shop out of her apartment in Williamsburg, Brooklyn.
Handmade ceramic jewelry, Instagram DMs, 50–60 orders a month. Business is good.
But tracking orders is a mess.
Maria herself writes: “pending”, “ready”, “sent out”, “arrived”. Her helper Jasmine writes: “wait”, “ready to ship”, “shipped”, “delivered”. Another helper, Carlos, writes: “waiting”, “prepped”, “in transit”, “got it”.
Come month-end, nobody can answer: how many orders are still pending? How many shipped? Nobody knows — because the same status has three different names.
This problem is called magic strings and the solution is called Enum.
1. What Is an Enum?
If Maria had told everyone upfront: “For order status, use only these five words: PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED. Nothing else.”
That fixed list of five words is an Enum (Enumeration).
An Enum is a special data type that holds a fixed, predefined set of named constants. Once defined, no value outside that set is possible.
from enum import Enum
class OrderStatus(Enum):
PENDING = "pending"
CONFIRMED = "confirmed"
SHIPPED = "shipped"
DELIVERED = "delivered"
CANCELLED = "cancelled"
Now if anyone types OrderStatus.SHIPED, Python throws an error immediately. No typos. No “in transit” vs “sent out”. Just five options.
Without Enum, maintaining this flow is hard — anyone can write any string, and invalid states slip through silently.
2. Simple Enum: When You Just Need Names
The simplest Enum just holds names. No extra data needed.
Think of a ride-share payment method — either CASH, CARD, or DIGITAL_WALLET (Venmo). Nothing outside those three.
from enum import Enum
class PaymentMethod(Enum):
CASH = 1
CARD = 2
DIGITAL_WALLET = 3
# Usage
method = PaymentMethod.DIGITAL_WALLET
print(method) # PaymentMethod.DIGITAL_WALLET
print(method.name) # DIGITAL_WALLET
print(method.value) # 3
# Comparison
if method == PaymentMethod.DIGITAL_WALLET:
print("Processing digital wallet payment")
# Loop over all members
for m in PaymentMethod:
print(m.name, "->", m.value)
# CASH -> 1
# CARD -> 2
# DIGITAL_WALLET -> 3
Enum members can be compared, looped over, and checked easily. Simple, readable, no surprises.
3. Enum with Properties and Methods: Smarter Enums
What if Maria’s order status needs to carry more information — a display label and a description alongside the code?
You can add properties and methods directly to an Enum.
from enum import Enum
class OrderStatus(Enum):
PENDING = ("pending", "Waiting", "Order received, not yet confirmed")
CONFIRMED = ("confirmed", "Confirmed", "Order confirmed, being packed")
SHIPPED = ("shipped", "Shipped", "Order handed to courier")
DELIVERED = ("delivered", "Delivered", "Customer has received the order")
CANCELLED = ("cancelled", "Cancelled", "Order has been cancelled")
def __new__(cls, code, label, description):
obj = object.__new__(cls)
obj._value_ = code
obj.label = label
obj.description = description
return obj
def is_active(self):
return self in (OrderStatus.PENDING, OrderStatus.CONFIRMED, OrderStatus.SHIPPED)
def can_cancel(self):
return self in (OrderStatus.PENDING, OrderStatus.CONFIRMED)
Now the Enum holds full information:
status = OrderStatus.SHIPPED
print(status.value) # shipped
print(status.label) # Shipped
print(status.description) # Order handed to courier
print(status.is_active()) # True
print(status.can_cancel()) # False — can't cancel once it's shipped
Each Enum member now knows for itself whether it can be cancelled or whether it’s still active. That logic lives inside the Enum — you don’t have to repeat it everywhere else.
Real-World Example: Amazon Order Processing
Amazon processes millions of orders a day. Each one moves through multiple states.
Without Enum, this system breaks fast:
# Without Enum: danger zone
order["status"] = "shiped" # typo — nobody catches it
order["status"] = "Delivered" # capital D — different string
order["status"] = "on the way" # someone went off-script
if order["status"] == "shipped": # this check will never pass!
notify_customer()
With Enum:
class Order:
def __init__(self, order_id: str, product: str):
self.order_id = order_id
self.product = product
self.status = OrderStatus.PLACED
def confirm(self):
if self.status == OrderStatus.PLACED:
self.status = OrderStatus.CONFIRMED
print(f"Order {self.order_id} confirmed")
def ship(self):
if self.status == OrderStatus.PACKED:
self.status = OrderStatus.SHIPPED
print(f"Order {self.order_id} shipped")
def cancel(self):
if self.status in (OrderStatus.PLACED, OrderStatus.CONFIRMED):
self.status = OrderStatus.CANCELLED
print(f"Order {self.order_id} cancelled")
else:
print(f"Cannot cancel in current state: {self.status.value}")
order = Order("AMZ-001", "Ceramic earrings")
order.confirm()
order.ship() # status isn't PACKED yet — nothing happens
order.cancel() # can't cancel after it's shipped
With Enum, invalid state transitions are impossible. No one can accidentally write order.status = "delvrd" and have it silently accepted.
Summary
| In the story | In code |
|---|---|
| Maria’s five fixed status words | Enum |
| Each specific word | Enum member |
| Storing a display label with each status | Enum property |
| “Can’t cancel once shipped” rule | Enum method |
| Anyone writing whatever they wanted | Magic strings (the problem) |
Using Enum stops invalid values at the compiler or runtime — before they cause bugs.
Enum members can carry properties and methods, keeping related logic in one place.
Code becomes self-documenting: OrderStatus.SHIPPED is instantly readable. 2 is not.
Next question: What if a class could hide some of its internals and only expose what outside callers need? That idea is called Encapsulation.
Next in the OOP series: Encapsulation — what doesn’t need to be seen, shouldn’t be