Command Pattern in Python: Undo/Redo Implementation

The Command pattern is a behavioral design pattern that turns requests into standalone objects. Instead of calling methods directly on receivers, you wrap the operation, its parameters, and the...

Key Insights

  • The Command pattern transforms operations into objects, enabling undo/redo by storing execution history in stacks and reversing operations on demand.
  • A well-designed CommandManager handles the undo/redo stack manipulation, clearing the redo stack when new commands execute to maintain consistency.
  • Each command must capture enough state at execution time to fully reverse itself—this is the critical detail most implementations get wrong.

Introduction to the Command Pattern

The Command pattern is a behavioral design pattern that turns requests into standalone objects. Instead of calling methods directly on receivers, you wrap the operation, its parameters, and the target into a command object that can be stored, passed around, and executed later.

This encapsulation is what makes undo/redo possible. When every operation is an object with both execute() and undo() methods, you can maintain a history of commands and reverse them in order.

Here’s the foundation:

from abc import ABC, abstractmethod
from typing import Any


class Command(ABC):
    """Base class for all commands."""
    
    @abstractmethod
    def execute(self) -> Any:
        """Execute the command."""
        pass
    
    @abstractmethod
    def undo(self) -> None:
        """Reverse the command's effects."""
        pass
    
    @property
    def description(self) -> str:
        """Human-readable description for UI/logging."""
        return self.__class__.__name__

The undo() method is what separates a command pattern implementation from a simple function wrapper. Every command must know how to reverse itself.

Anatomy of the Command Pattern

Four components make up the pattern:

  • Command: The abstract interface defining execute() and undo()
  • Concrete Commands: Implementations that perform specific operations
  • Receiver: The object being acted upon (e.g., a document, canvas, or database)
  • Invoker: Manages command execution and history (our CommandManager)
# Component relationships:
#
# Client creates ConcreteCommand with Receiver reference
#        │
#        ▼
#   ┌─────────────┐     executes    ┌─────────────┐
#   │   Invoker   │ ───────────────▶│   Command   │
#   │ (Manager)   │                 │ (Abstract)  │
#   └─────────────┘                 └─────────────┘
#        │                                │
#   stores in                        implements
#   history                               │
#        │                                ▼
#        │                        ┌─────────────┐
#        │                        │  Concrete   │
#        │                        │  Command    │
#        │                        └─────────────┘
#        │                                │
#        │                          calls │
#        │                                ▼
#        │                        ┌─────────────┐
#        └───────────────────────▶│  Receiver   │
#                                 └─────────────┘

This decoupling means the invoker doesn’t need to know anything about what commands actually do. It just calls execute() and undo(). This makes testing straightforward—you can test commands in isolation with mock receivers.

Building the Undo/Redo Stack

The CommandManager is where the magic happens. It maintains two stacks: one for commands that can be undone, another for commands that can be redone.

The critical rule: executing a new command clears the redo stack. If you undo three actions, then perform a new action, those three undone commands are gone. This matches user expectations from every text editor and design tool.

from typing import Optional


class CommandManager:
    """Manages command execution with undo/redo support."""
    
    def __init__(self, max_history: int = 100):
        self._undo_stack: list[Command] = []
        self._redo_stack: list[Command] = []
        self._max_history = max_history
    
    def execute(self, command: Command) -> Any:
        """Execute a command and add it to history."""
        result = command.execute()
        self._undo_stack.append(command)
        self._redo_stack.clear()  # Critical: new action invalidates redo history
        
        # Prevent unbounded memory growth
        if len(self._undo_stack) > self._max_history:
            self._undo_stack.pop(0)
        
        return result
    
    def undo(self) -> Optional[Command]:
        """Undo the last command. Returns the undone command or None."""
        if not self._undo_stack:
            return None
        
        command = self._undo_stack.pop()
        command.undo()
        self._redo_stack.append(command)
        return command
    
    def redo(self) -> Optional[Command]:
        """Redo the last undone command. Returns the redone command or None."""
        if not self._redo_stack:
            return None
        
        command = self._redo_stack.pop()
        command.execute()
        self._undo_stack.append(command)
        return command
    
    @property
    def can_undo(self) -> bool:
        return len(self._undo_stack) > 0
    
    @property
    def can_redo(self) -> bool:
        return len(self._redo_stack) > 0

Note the max_history parameter. Without bounds, your undo stack will grow indefinitely. For a text editor handling thousands of keystrokes, this matters.

Practical Implementation: Text Editor

Let’s build a minimal text editor with insert, delete, and replace operations. The receiver is a TextDocument class:

class TextDocument:
    """The receiver: a simple text document."""
    
    def __init__(self, content: str = ""):
        self._content = content
    
    @property
    def content(self) -> str:
        return self._content
    
    def insert(self, position: int, text: str) -> None:
        self._content = self._content[:position] + text + self._content[position:]
    
    def delete(self, position: int, length: int) -> str:
        """Delete text and return what was deleted."""
        deleted = self._content[position:position + length]
        self._content = self._content[:position] + self._content[position + length:]
        return deleted
    
    def __len__(self) -> int:
        return len(self._content)

Now the concrete commands. Each command captures the state needed to reverse itself:

class InsertTextCommand(Command):
    """Insert text at a specific position."""
    
    def __init__(self, document: TextDocument, position: int, text: str):
        self._document = document
        self._position = position
        self._text = text
    
    def execute(self) -> None:
        self._document.insert(self._position, self._text)
    
    def undo(self) -> None:
        # To undo an insert, delete the same text
        self._document.delete(self._position, len(self._text))
    
    @property
    def description(self) -> str:
        preview = self._text[:20] + "..." if len(self._text) > 20 else self._text
        return f"Insert '{preview}' at position {self._position}"


class DeleteTextCommand(Command):
    """Delete text at a specific position."""
    
    def __init__(self, document: TextDocument, position: int, length: int):
        self._document = document
        self._position = position
        self._length = length
        self._deleted_text: str = ""  # Captured during execute
    
    def execute(self) -> str:
        # Capture the deleted text so we can restore it
        self._deleted_text = self._document.delete(self._position, self._length)
        return self._deleted_text
    
    def undo(self) -> None:
        # To undo a delete, insert the captured text back
        self._document.insert(self._position, self._deleted_text)
    
    @property
    def description(self) -> str:
        preview = self._deleted_text[:20] + "..." if len(self._deleted_text) > 20 else self._deleted_text
        return f"Delete '{preview}' at position {self._position}"


class ReplaceTextCommand(Command):
    """Replace text at a specific position."""
    
    def __init__(self, document: TextDocument, position: int, length: int, new_text: str):
        self._document = document
        self._position = position
        self._length = length
        self._new_text = new_text
        self._old_text: str = ""  # Captured during execute
    
    def execute(self) -> None:
        self._old_text = self._document.delete(self._position, self._length)
        self._document.insert(self._position, self._new_text)
    
    def undo(self) -> None:
        self._document.delete(self._position, len(self._new_text))
        self._document.insert(self._position, self._old_text)

Notice how DeleteTextCommand captures _deleted_text during execute(). This is essential—you can’t undo a deletion without knowing what was deleted.

Handling Complex Undo Scenarios

Real applications often need to treat multiple commands as a single undoable action. Find-and-replace-all, for example, might make dozens of replacements that should undo together.

class MacroCommand(Command):
    """A composite command that groups multiple commands."""
    
    def __init__(self, commands: list[Command], description: str = "Macro"):
        self._commands = commands
        self._description = description
    
    def execute(self) -> None:
        for command in self._commands:
            command.execute()
    
    def undo(self) -> None:
        # Undo in reverse order
        for command in reversed(self._commands):
            command.undo()
    
    @property
    def description(self) -> str:
        return self._description


# Usage: Find and replace all
def find_replace_all(
    document: TextDocument, 
    find: str, 
    replace: str
) -> MacroCommand:
    """Create a macro command for find/replace all."""
    commands = []
    content = document.content
    position = 0
    
    while True:
        index = content.find(find, position)
        if index == -1:
            break
        
        # Account for length changes from previous replacements
        offset = sum(
            len(replace) - len(find) 
            for cmd in commands
        )
        
        commands.append(ReplaceTextCommand(
            document, 
            index + offset, 
            len(find), 
            replace
        ))
        position = index + len(find)
    
    return MacroCommand(commands, f"Replace all '{find}' with '{replace}'")

For memory optimization with large histories, consider storing deltas or snapshots at intervals rather than every command:

class CheckpointManager:
    """Manages periodic snapshots for memory efficiency."""
    
    def __init__(self, document: TextDocument, checkpoint_interval: int = 50):
        self._document = document
        self._interval = checkpoint_interval
        self._checkpoints: dict[int, str] = {0: document.content}
        self._command_count = 0
    
    def record_command(self) -> None:
        self._command_count += 1
        if self._command_count % self._interval == 0:
            self._checkpoints[self._command_count] = self._document.content
    
    def restore_checkpoint(self, command_index: int) -> Optional[str]:
        """Find nearest checkpoint at or before the given index."""
        valid_checkpoints = [k for k in self._checkpoints if k <= command_index]
        if not valid_checkpoints:
            return None
        nearest = max(valid_checkpoints)
        return self._checkpoints[nearest]

Testing Command-Based Systems

Commands are highly testable because they’re self-contained. Test each command in isolation, then test sequences:

import pytest


class TestTextCommands:
    """Unit tests for text editor commands."""
    
    def test_insert_execute(self):
        doc = TextDocument("Hello World")
        cmd = InsertTextCommand(doc, 5, " Beautiful")
        cmd.execute()
        assert doc.content == "Hello Beautiful World"
    
    def test_insert_undo(self):
        doc = TextDocument("Hello World")
        cmd = InsertTextCommand(doc, 5, " Beautiful")
        cmd.execute()
        cmd.undo()
        assert doc.content == "Hello World"
    
    def test_delete_captures_text(self):
        doc = TextDocument("Hello Beautiful World")
        cmd = DeleteTextCommand(doc, 5, 10)
        deleted = cmd.execute()
        assert deleted == " Beautiful"
        assert doc.content == "Hello World"
    
    def test_delete_undo_restores(self):
        doc = TextDocument("Hello Beautiful World")
        cmd = DeleteTextCommand(doc, 5, 10)
        cmd.execute()
        cmd.undo()
        assert doc.content == "Hello Beautiful World"


class TestCommandManager:
    """Integration tests for undo/redo sequences."""
    
    def test_undo_redo_cycle(self):
        doc = TextDocument("")
        manager = CommandManager()
        
        manager.execute(InsertTextCommand(doc, 0, "Hello"))
        manager.execute(InsertTextCommand(doc, 5, " World"))
        assert doc.content == "Hello World"
        
        manager.undo()
        assert doc.content == "Hello"
        
        manager.redo()
        assert doc.content == "Hello World"
    
    def test_new_command_clears_redo(self):
        doc = TextDocument("")
        manager = CommandManager()
        
        manager.execute(InsertTextCommand(doc, 0, "A"))
        manager.execute(InsertTextCommand(doc, 1, "B"))
        manager.undo()
        
        assert manager.can_redo
        
        manager.execute(InsertTextCommand(doc, 1, "C"))
        
        assert not manager.can_redo  # Redo stack cleared
        assert doc.content == "AC"

When to Use (and Avoid) This Pattern

Use the Command pattern when:

  • You need undo/redo functionality
  • You want to queue, log, or schedule operations
  • You need transaction-like behavior with rollback
  • You’re building GUI applications with action history
  • You’re implementing game replay systems

Avoid it when:

  • Operations are simple and don’t need reversal
  • The overhead of command objects isn’t justified
  • State restoration is simpler with the Memento pattern (full snapshots)

The Memento pattern is an alternative when commands are hard to reverse. Instead of storing operations, you store complete state snapshots. This uses more memory but handles complex state changes that are difficult to reverse incrementally.

For most undo/redo implementations, the Command pattern is the right choice. It’s memory-efficient, testable, and maps naturally to user actions. Start with the basic structure shown here, then extend with macro commands and memory optimization as your application demands.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.