Course contents

State — behavior varies with internal state

Sign in to run this lab and save your progress

Learn

Before you start

State is one of the most under-taught patterns. Most bootcamps mention it once. Meanwhile, half the "if/elif on a state field" code in the industry could be the State pattern.

Learn it here; use it constantly.

The one-line version

State turns an object's mode-of-operation into its own class, with the behavior for that mode living there. The object holds a reference to its current state and delegates.

The signal to use State

  • Multiple modes / phases (3+) with different behavior per mode
  • Multiple actions (2+) that behave differently in different modes
  • Explicit transition rules — only certain mode-changes are allowed
  • State transitions triggered by actions — the object moves from A to B in response to some call

Any 2-3 of these → State.

Where State shows up in real design problems

Nearly EVERY design problem has a state machine somewhere:

  • Vending Machine (this lesson)
  • Elevator — moving up, moving down, stopped, doors-open
  • TCP connection — LISTEN, SYN_SENT, ESTABLISHED, CLOSING
  • Order lifecycle — PENDING, PAID, SHIPPED, DELIVERED, RETURNED, REFUNDED
  • Media player — STOPPED, PLAYING, PAUSED
  • UI wizard — step 1, step 2, step 3, complete, error
  • Booking flow — search, hold, payment, confirmation
  • Game character — idle, running, jumping, attacking, dead
  • Chess piece (unusual states: en passant, castling eligibility)
  • Traffic light — red, yellow, green

If you catch yourself writing an if self.status == "X": branch in more than one method — you've spotted a State opportunity.

The State vs Strategy question

Both hold a "swappable behavior" object. The real difference:

  • Strategy — the caller PICKS which strategy is active. It doesn't change unless the caller changes it.
  • State — the object TRANSITIONS between states in response to actions. Each state knows about the next state.

Quick test: does the "which flavor?" decision come from outside (Strategy) or from the object itself (State)? Different patterns; often confused.

Where do transitions live?

Two schools:

  • Inside the state class (this lesson's approach) — each state knows which state to transition to for each action. Pro: transitions live close to the behavior that causes them. Con: states know about other states.
  • Outside — a TransitionManager decides based on the current state and action. Pro: transitions all in one place. Con: state classes get dumber; the manager gets smart in a way that recreates the if/elif smell.

For interviews, "inside the state class" is the standard answer. For very complex state machines (10+ states), a transition table can be cleaner.

The "invalid transition" question

Two schools:

  • Reject silently (return a string / status) — our lesson. Not fatal; the caller can react.
  • Raise an exception — invalid transitions are bugs. Fail loudly.

Both are valid. Depends on the problem. For a vending machine, quiet rejection with a user-visible message ("insert coin first") is right. For a TCP state machine, invalid transitions are protocol violations — raise.

The AST test

Our test AST-scans VendingMachine's methods for isinstance or if statements. If any snuck in, the pattern's payoff is gone. The whole point of State is to move all the branching INTO the state classes — the outer object should be a thin dispatcher.

The decision cheat-sheet

Situation Pattern
Object mode changes based on its actions State
Algorithm chosen from outside Strategy
Actions need undo Command
Complex transitions with guards + actions on edges Full state machine library
Only 2 states, 1 action Just use a bool
Behavior depends on TWO state variables Break into two state machines OR use a state table

Your task

State is the pattern for objects whose behavior CHANGES over time based on what state they're in. Instead of one class with a giant if/elif on a state field, State gives each state its own class — with the behavior for that state living there.

The problem

You're building a vending machine. It goes through phases:

  • Idle — waiting for a coin. Accepts coins. Rejects dispense requests.
  • HasCoin — coin inserted. Accepts a product selection. Rejects more coins.
  • Dispensing — product being handed out. Rejects everything until it goes back to Idle.

Naive first attempt — one class with a state field:

class VendingMachine:
    def __init__(self):
        self.state = "idle"
        self.coin_count = 0

    def insert_coin(self):
        if self.state == "idle":
            self.state = "has_coin"
            self.coin_count += 1
            return "ok"
        elif self.state == "has_coin":
            return "already have coin"
        elif self.state == "dispensing":
            return "busy dispensing"

    def select(self, product):
        if self.state == "idle":
            return "insert coin first"
        elif self.state == "has_coin":
            self.state = "dispensing"
            return f"dispensing {product}"
        elif self.state == "dispensing":
            return "busy"

    def finish_dispense(self):
        if self.state == "dispensing":
            self.state = "idle"
            return "ready"
        else:
            return "not dispensing"

Every method has an if/elif chain on the state field. Adding a state (like Refilling for maintenance) means editing every method. Adding an action means editing every state branch. Fragile.

Questions to ask about the requirements

  1. How many states are there? 2 → probably don't need State. 3+ → State starts paying off. 5+ → State pattern is almost certainly the right answer.
  2. How many ACTIONS does the object support? More actions × more states = more if/elif branches. The pattern's payoff grows with the product.
  3. Do state transitions have RULES? Only certain transitions are valid (Idle → HasCoin, HasCoin → Dispensing, Dispensing → Idle). If so, State makes those rules easy to see.
  4. Do different states behave DIFFERENTLY for the same action? (An insert_coin means "accept" in Idle, "reject" in HasCoin.) If yes, State fits.
  5. Would forgetting to handle a state be a real bug? State's polymorphism makes it impossible to forget one (each state class has to implement every method).

For the vending machine: 3+ states, multiple actions, strict transition rules, different behavior per state. State fits.

Different designs, compared

Design A — if/elif on a state field

The naive version above.

  • Good: One class. Familiar shape.
  • Bad: Every method touches every state. Adding a state means editing every method. State transitions are scattered — hard to see the state machine as a whole.
  • When it wins: 2 states, 1-2 actions. Rare.

Design B — enum + big switch

Same as A, just uses an Enum instead of strings.

  • Good: Type safety (can't typo "idel").
  • Bad: Same problem. Just tidier strings.

Design C — State pattern

Each state gets its own class. Each state class implements every action. Actions return the NEXT state.

class State(ABC):
    @abstractmethod
    def insert_coin(self, machine): ...
    @abstractmethod
    def select(self, machine, product): ...
    @abstractmethod
    def finish_dispense(self, machine): ...

class IdleState(State):
    def insert_coin(self, machine):
        machine._state = HasCoinState()
        return "ok"
    def select(self, machine, product):
        return "insert coin first"
    def finish_dispense(self, machine):
        return "not dispensing"

class HasCoinState(State):
    def insert_coin(self, machine):
        return "already have coin"
    def select(self, machine, product):
        machine._state = DispensingState()
        return f"dispensing {product}"
    def finish_dispense(self, machine):
        return "not dispensing"

class DispensingState(State):
    def insert_coin(self, machine):
        return "busy dispensing"
    def select(self, machine, product):
        return "busy"
    def finish_dispense(self, machine):
        machine._state = IdleState()
        return "ready"

class VendingMachine:
    def __init__(self):
        self._state = IdleState()

    def insert_coin(self):
        return self._state.insert_coin(self)

    def select(self, product):
        return self._state.select(self, product)

    def finish_dispense(self):
        return self._state.finish_dispense(self)
  • Good: Each state's behavior lives in one class. Adding a state = new class implementing the interface. State transitions are explicit and easy to trace. VendingMachine is a thin wrapper.
  • Bad: More classes. Each state has to implement every action, even just to reject it.
  • When it wins: Multiple states, multiple actions, transition rules matter.

The locked-in choice: Design C

Reviewing the requirements:

  • 3+ states with different behavior per state. → Strong signal for C over A/B.
  • Multiple actions per state. → C removes the if/elif explosion (states × actions).

Design C wins.

State vs Strategy

Both hold a "swappable behavior" object. Difference:

  • Strategy — the caller CHOOSES which strategy is active. Strategy doesn't change on its own.
  • State — the object TRANSITIONS between states in response to actions. State knows about next-state.

Strategy is external choice; State is internal transition. Both are polymorphism-plus-composition; different intent.

Where State shows up in real LLD problems

  • Vending machine (this lesson)
  • TCP connection lifecycle — LISTEN → SYN_SENT → ESTABLISHED → …
  • Order lifecycle — PENDING → PAID → SHIPPED → DELIVERED
  • Booking flow — SEARCHING → HOLD → PAYMENT → CONFIRMED
  • Game character — IDLE → RUNNING → JUMPING → FALLING
  • Media player — STOPPED → PLAYING → PAUSED
  • UI wizards — step 1 → step 2 → step 3

Any object that "goes through phases" → State candidate.

Your task

In main.py build:

  1. Abstract State(ABC) with:
    • insert_coin(self, machine) → str
    • select(self, machine, product) → str
    • finish_dispense(self, machine) → str
  2. Three concrete states — IdleState, HasCoinState, DispensingState — each implementing all three methods with the behavior described in the objective.
  3. VendingMachine:
    • __init__self._state = IdleState()
    • insert_coin() / select(product) / finish_dispense() — all delegate to self._state
    • current_state() — returns the state class's __name__ string (for tests)

Transitions: Idle -- insert_coin --> HasCoin HasCoin -- select --> Dispensing Dispensing -- finish --> Idle

All other actions in each state are rejected with the strings from the naive version.

Structural tests enforce:

  • Full valid transition cycle works.
  • Every invalid action returns the expected rejection string and does NOT change state.
  • VendingMachine has no if/elif on state (AST check).

Click Submit to run the tests.

Starter files

Preview only — open the lab to edit and run.

files_dir
14-state-starter