The State pattern — an object that genuinely behaves differently depending on its state
A pile of if/elif checks on a status field scattered across every method is a real, common way OOP code degrades — State pulls each status's actual behavior into its own class, so "what happens in this state" lives in exactly one place instead of being re-checked everywhere.
3 min read
The problem: a status field, checked everywhere it matters
class Order:
def __init__(self):
self.status = "pending"
def ship(self):
if self.status == "pending":
self.status = "shipped"
elif self.status == "shipped":
raise Exception("Already shipped")
elif self.status == "cancelled":
raise Exception("Cannot ship a cancelled order")
def cancel(self):
if self.status == "pending":
self.status = "cancelled"
elif self.status == "shipped":
raise Exception("Cannot cancel a shipped order")
elif self.status == "cancelled":
raise Exception("Already cancelled")Every method that behaves differently depending on status needs its own if/elif chain checking that same field — and every time a new status is added ("returned", "refunded"), every one of these chains, in every method, needs a new branch added to it. This is a real, common way object-oriented code degrades over time: the object's actual behavior is scattered across every method's own copy of "what should happen in each state," instead of living in one place per state.
The fix: one class per state, each implementing the same interface
class OrderState:
def ship(self, order): raise NotImplementedError
def cancel(self, order): raise NotImplementedError
class PendingState(OrderState):
def ship(self, order):
order.state = ShippedState()
def cancel(self, order):
order.state = CancelledState()
class ShippedState(OrderState):
def ship(self, order):
raise Exception("Already shipped")
def cancel(self, order):
raise Exception("Cannot cancel a shipped order")
class CancelledState(OrderState):
def ship(self, order):
raise Exception("Cannot ship a cancelled order")
def cancel(self, order):
raise Exception("Already cancelled")
class Order:
def __init__(self):
self.state = PendingState() # DELEGATES to whichever state object is current
def ship(self):
self.state.ship(self)
def cancel(self):
self.state.cancel(self)Each state is now its own class, implementing the same interface (ship, cancel), and Order simply delegates every call to whatever state object it currently holds — Order.ship() doesn't contain any if logic about status at all; it just asks the current state object what to do, and that state object knows both its own behavior and which state comes next. Adding a new status means adding one new class implementing the shared interface, not touching every existing method's if/elif chain.
State transitions live inside the state classes themselves, not the context
class PendingState(OrderState):
def ship(self, order):
order.state = ShippedState() # THIS state decides what the NEXT state isNotice PendingState.ship() is what sets order.state = ShippedState() — the transition logic ("shipping a pending order moves it to shipped") lives inside the state that's transitioning out, not inside Order itself. This keeps Order genuinely simple (just delegation) while each state class fully owns both its own behavior and its own valid next-states, which is exactly the information that used to be smeared across Order's own if/elif chains.
How this differs from Strategy, since both swap out behavior at runtime
Strategy (from its own lesson) swaps between interchangeable algorithms chosen externally, usually once, by whoever constructs the object — the object using a strategy doesn't know or care which one it has, and the strategies don't transition into each other. State is different in a real, structural way: the states themselves control the transitions between each other (as shown above), and the whole point is that the object's behavior changes as a direct consequence of its own internal state changing over time — not an external choice made once and left alone.
The concrete signal a State pattern belongs somewhere
The tell: an object has a status/mode field, and multiple methods each branch on that field's value, with the same set of cases repeated across methods — especially if new statuses get added over time and each addition means touching several existing methods. A simple two-state toggle (is_active: bool) rarely needs this; the pattern earns its keep once there are three or more states with real, distinct behavior and transition rules between them.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. What real problem does scattering `if status == 'pending': ... elif status == 'shipped': ...` across multiple methods cause?
2. In the State pattern, where does the logic for transitioning FROM one state TO the next actually live?
3. What's the structural difference between State and Strategy, since both swap behavior at runtime?