Have you ever used an undo button in an app or scheduled tasks to run later? Both of these rely on the same idea: turning actions into objects.
That's the command pattern. Instead of calling a method directly, you package the call – the action, its target, and any arguments – into an object. That object can be stored, passed around, executed later, or undone.
In this tutorial, you'll learn what the command pattern is and how to implement it in Python with a practical text editor example that supports undo.
You can find the code for this tutorial on GitHub.
Prerequisites
Before we start, make sure you have:
Python 3.10 or higher installed
Basic understanding of Python classes and methods
Familiarity with object-oriented programming (OOP) concepts
Let's get started!
Table of Contents
What Is the Command Pattern?
The command pattern is a behavioral design pattern that encapsulates a request as an object. This lets you:
Parameterize callers with different operations
Queue or schedule operations for later execution
Support undo/redo by keeping a history of executed commands
The pattern has four key participants:
Command: an interface with an
execute()method (and optionallyundo())Concrete Command: implements
execute()andundo()for a specific actionReceiver: the object that actually does the work (for example, a document)
Invoker: triggers commands and manages history
Think of a restaurant. The customer (client) tells the waiter (invoker) what they want. The waiter writes it on a ticket (command) and hands it to the kitchen (receiver). The waiter doesn't cook – they only manage tickets. If you change your mind, the waiter can cancel the ticket before it reaches the kitchen.
Setting Up the Receiver
We'll build a simple document editor. The receiver here is the Document class. It knows how to insert and delete text, but it has no idea who's calling it or why.
class Document:
def __init__(self):
self.content = ""
def insert(self, text: str, position: int) -> None:
self.content = (
self.content[:position] + text + self.content[position:]
)
def delete(self, position: int, length: int) -> None:
self.content = (
self.content[:position] + self.content[position + length:]
)
def show(self) -> None:
print(f'Document: "{self.content}"')
insert places text at a given position. delete removes length characters from a given position. Both are plain methods with no history or awareness of commands. And that's intentional.
Defining Commands
Now let's define a base Command interface using an abstract class:
from abc import ABC, abstractmethod
class Command(ABC):
@abstractmethod
def execute(self) -> None:
pass
@abstractmethod
def undo(self) -> None:
pass
Any concrete command must implement both execute and undo. This is what makes a full history possible.
InsertCommand
InsertCommand stores the text and position at creation time:
class InsertCommand(Command):
def __init__(self, document: Document, text: str, position: int):
self.document = document
self.text = text
self.position = position
def execute(self) -> None:
self.document.insert(self.text, self.position)
def undo(self) -> None:
self.document.delete(self.position, len(self.text))
When execute() is called, it inserts the text. When undo() is called, it deletes exactly what was inserted. Notice that undo is the inverse of execute – this is the key design requirement.
DeleteCommand
Now let's code the DeleteCommand:
class DeleteCommand(Command):
def __init__(self, document: Document, position: int, length: int):
self.document = document
self.position = position
self.length = length
self._deleted_text = "" # stored on execute, used on undo
def execute(self) -> None:
self._deleted_text = self.document.content[
self.position : self.position + self.length
]
self.document.delete(self.position, self.length)
def undo(self) -> None:
self.document.insert(self._deleted_text, self.position)
DeleteCommand has one important detail: it captures the deleted text during execute(), not at creation time. This is because we don't know what text is at that position until the command actually runs. Without this, undo() wouldn't know what to restore.
The Invoker: Running and Undoing Commands
The invoker is the object that executes commands and keeps a history stack. It has no idea what a document is or how text editing works. It just manages command objects.
class EditorInvoker:
def __init__(self):
self._history: list[Command] = []
def run(self, command: Command) -> None:
command.execute()
self._history.append(command)
def undo(self) -> None:
if not self._history:
print("Nothing to undo.")
return
command = self._history.pop()
command.undo()
print("Undo successful.")
run() executes the command and pushes it onto the history stack. undo() pops the last command and calls its undo() method. The stack naturally gives you the right order: last in, first undone.
Putting It All Together
Let's put it all together and walk through a real editing session:
doc = Document()
editor = EditorInvoker()
# Type a title
editor.run(InsertCommand(doc, "Quarterly Report", 0))
doc.show()
# Add a subtitle
editor.run(InsertCommand(doc, " - Finance", 16))
doc.show()
# Oops, wrong subtitle — undo it
editor.undo()
doc.show()
# Delete "Quarterly" and replace with "Annual"
editor.run(DeleteCommand(doc, 0, 9))
doc.show()
editor.run(InsertCommand(doc, "Annual", 0))
doc.show()
# Undo the insert
editor.undo()
doc.show()
# Undo the delete (restores "Quarterly")
editor.undo()
doc.show()
This outputs:
Document: "Quarterly Report"
Document: "Quarterly Report - Finance"
Undo successful.
Document: "Quarterly Report"
Document: " Report"
Document: "Annual Report"
Undo successful.
Document: " Report"
Undo successful.
Document: "Quarterly Report"
Here's the step-by-step breakdown of how (and why) this works:
Each
InsertCommandandDeleteCommandcarries its own instructions for both doing and undoing.EditorInvokernever looks inside a command. It only callsexecute()andundo().The document (
Document) never thinks about history. It mutates its content when told to.
Each participant has a single, clear responsibility.
Extending with Macros
One of the lesser-known benefits of the command pattern is that commands are just objects. So you can group them. Here's a MacroCommand that batches several commands and undoes them as a unit:
class MacroCommand(Command):
def __init__(self, commands: list[Command]):
self.commands = commands
def execute(self) -> None:
for cmd in self.commands:
cmd.execute()
def undo(self) -> None:
for cmd in reversed(self.commands):
cmd.undo()
# Apply a heading format in one shot: clear content, insert formatted title
macro = MacroCommand([
DeleteCommand(doc, 0, len(doc.content)),
InsertCommand(doc, "== Annual Report ==", 0),
])
editor.run(macro)
doc.show()
editor.undo()
doc.show()
This gives the following output:
Document: "== Annual Report =="
Undo successful.
Document: "Quarterly Report"
The macro undoes its commands in reverse order. This is correct since the last thing done should be the first thing undone.
When to Use the Command Pattern
The command pattern is a good fit when:
You need undo/redo: the pattern is practically made for this. Store executed commands in a stack and reverse them.
You need to queue or schedule operations: commands are objects, so you can put them in a queue, serialize them, or delay execution.
You want to decouple the caller from the action: the invoker doesn't need to know what the command does. It just runs it.
You need to support macros or batched operations: group commands into a composite and run them together, as shown above.
Avoid it when:
The operations are simple and will never need undo or queuing. The pattern adds classes and indirection that may not be worth it for a simple CRUD action.
Commands would need to share so much state that the "encapsulate the request" idea breaks down.
Conclusion
I hope you found this tutorial useful. To summarize, the command pattern turns actions into objects. And that single idea unlocks a lot: undo/redo, queuing, macros, and clean separation between who triggers an action and what the action does.
We built a document editor from scratch using InsertCommand, DeleteCommand, an EditorInvoker with a history stack, and a MacroCommand for batched edits. Each class knew exactly one thing and did it well.
As a next step, try extending the editor with a RedoCommand. You'll need a second stack alongside the history to bring back undone commands.
Happy coding!