ExampleDifficulty: Intermediate2 min

State Example: A Document Moving From Draft to Published

publish() means submit for review in Draft, approve in Review, and nothing in Published.

A document with a review step is a natural State, and this example maps the template onto it. The document starts as a draft. Publishing a draft submits it for review; publishing from review approves it; publishing a published document does nothing. Rejecting is possible only in review, and sends the document back to draft. Each state class writes its own transitions.

The template's parts in the document example
TemplateDocument example
ContextDocument
StateDocumentState, with publish and a default reject that ignores the request
ConcreteStateDraft, Review and Published
TransitionsSet by the state classes: lines 15, 21 and 25
py
from abc import ABC, abstractmethod


class DocumentState(ABC):
    @abstractmethod
    def publish(self, document: "Document") -> None: ...

    def reject(self, document: "Document") -> None:
        print(f"  reject ignored in {type(self).__name__}")


class Draft(DocumentState):
    def publish(self, document: "Document") -> None:
        print("  submitted for review")
        document.state = Review()


class Review(DocumentState):
    def publish(self, document: "Document") -> None:
        print("  approved and published")
        document.state = Published()

    def reject(self, document: "Document") -> None:
        print("  sent back to draft")
        document.state = Draft()


class Published(DocumentState):
    def publish(self, document: "Document") -> None:
        print("  already published, nothing to do")


class Document:
    def __init__(self) -> None:
        self.state: DocumentState = Draft()

    def publish(self) -> None:
        self.state.publish(self)

    def reject(self) -> None:
        self.state.reject(self)


doc = Document()
for action in ("publish", "reject", "publish", "publish", "publish", "reject"):
    print(f"{action} in {type(doc.state).__name__}:")
    getattr(doc, action)()
print("final state:", type(doc.state).__name__)
OutputRan with python:3.12-slim (Python 3.12.13)
publish in Draft:
  submitted for review
reject in Review:
  sent back to draft
publish in Draft:
  submitted for review
publish in Review:
  approved and published
publish in Published:
  already published, nothing to do
reject in Published:
  reject ignored in Published
final state: Published
The same two methods, called six times, do four different things.

The output follows the state diagram arrow by arrow: submitted, sent back, submitted again, approved, then nothing for the two requests that have no arrow out of Published. Document has no if in it, and neither does the loop at the bottom.

The base class gives reject a default that ignores the request, so only Review, the one state with a reject arrow, overrides it. Adding an "archived" state would be one new class and a change to Published.publish; no existing check would need finding, because there are none.