Using the Stack Data Structure in Python

In computer science, data structures organize, manage, and store data for various operations. One essential and versatile structure is a stack.

A stack is a collection of items that follows the Last-In-First-Out (LIFO) principle. This means the last item added is the first one to be removed.

This structure plays a critical role in many programming scenarios. If you’ve ever dealt with an “undo” feature in an application or tracked the sequence of nested calls in programming, you’ve seen stacks in action.

In this article, we’ll dive into what a stack is, create one in Python, and explore a real-world example using a fruit inventory system.

Why Use a Stack Class?

The purpose of a stack class is to encapsulate the stack’s functionality, making it reusable and easy to modify. By creating a class, you define how the stack operates (e.g., how items are added, removed, or checked). This abstraction prevents errors and allows the stack to integrate seamlessly into larger programs.

Real-World Applications of a Stack

The stack structure is not just theoretical. It has practical applications that are deeply embedded in everyday software functionality. Below are three real-world examples where stacks shine, complete with Python implementations.

Setting up the Stack Class:

This is a Python class named Stack that provides a blueprint for creating and managing stack objects. It encapsulates the stack’s functionality, including adding items (push), removing items (pop), checking the top item (peek), and checking whether the stack is empty (is_empty).

The stack uses a Python list internally to store its items, adhering to the Last-In-First-Out (LIFO) principle.

# File: stack_class.py
class Stack:
    def __init__(self):
        self.__index = []  # Internal list to store stack items

    def push(self, item):
        """Add an item to the top of the stack."""
        self.__index.append(item)

    def pop(self):
        """Remove and return the top item from the stack."""
        if not self.is_empty():
            return self.__index.pop()
        raise IndexError("pop from an empty stack")

    def peek(self):
        """Return the top item without removing it."""
        if not self.is_empty():
            return self.__index[-1]
        raise IndexError("peek from an empty stack")

    def is_empty(self):
        """Check if the stack is empty."""
        return len(self.__index) == 0

    def __len__(self):
        """Return the number of items in the stack."""
        return len(self.__index)

What does it do?

The class provides methods that implement common stack operations:

Initialization (__init__):

  • Initializes an empty list (self.__index) to store the stack’s items.
def __init__(self):
    self.__index = []  # Internal list to store stack items

Push (push):

  • Adds an item to the top of the stack.
  • Appends the item to the internal list.
def push(self, item):
    self.__index.append(item)

Pop (pop):

  • Removes and returns the top item from the stack.
  • Uses Python’s list.pop() to remove the last item.
  • Raises an IndexError if the stack is empty.
def pop(self):
    if not self.is_empty():
        return self.__index.pop()
    raise IndexError("pop from an empty stack")

Peek (peek):

  • Returns the top item without removing it.
  • Accesses the last item in the list (self.__index[-1]).
  • Raises an IndexError if the stack is empty.
def peek(self):
    if not self.is_empty():
        return self.__index[-1]
    raise IndexError("peek from an empty stack")

Check if Empty (is_empty):

  • Returns True if the stack is empty, False otherwise.
def is_empty(self):
    return len(self.__index) == 0

Get Stack Size (__len__):

  • Returns the number of items in the stack using len(self.__index).
  • Allows you to use the built-in len() function on a stack instance.
def __len__(self):
    return len(self.__index)

While the Stack above is not universal, it is highly portable and sufficient for most stack-related tasks in Python.

1. Undo/Redo Functionality in Text Editors

In text editors, actions like typing, deleting, or formatting are stored in a stack. When you press “Undo,” the last action is ‘popped off’ the stack and reversed. If you then press “Redo,” the undone action is moved to a separate redo stack and reapplied.

Implementation:

# File: text_editor.py
from stack_class import Stack

class UndoRedoStack:
    def __init__(self):
        self.undo_stack = Stack()
        self.redo_stack = Stack()

    def perform_action(self, action):
        """Perform an action and add it to the undo stack."""
        self.undo_stack.push(action)
        self.redo_stack = Stack()  # Clear redo stack after a new action

    def undo(self):
        """Undo the last action and move it to the redo stack."""
        if not self.undo_stack.is_empty():
            action = self.undo_stack.pop()
            self.redo_stack.push(action)
            return f"Undid action: {action}"
        return "Nothing to undo"

    def redo(self):
        """Redo the last undone action."""
        if not self.redo_stack.is_empty():
            action = self.redo_stack.pop()
            self.undo_stack.push(action)
            return f"Redid action: {action}"
        return "Nothing to redo"

# Usage example:
editor = UndoRedoStack()
editor.perform_action("Typed 'Hello'")
editor.perform_action("Deleted 'o'")
print(editor.undo())  # Output: Undid action: Deleted 'o'
print(editor.redo())  # Output: Redid action: Deleted 'o'

This demonstrates how stacks allow reverse and reapply actions, a critical feature in productivity tools. The code simulated deleting the “o” in Hello. Then the Stack performed an undo and redo.

2. Call Stack in Programming

When a program calls a function, the function’s state (its arguments and local variables) is stored on the call stack. This ensures that once the function completes, the program knows where to resume execution. Recursive functions, in particular, rely heavily on the call stack.

Implementation:

# File: call_stack_example.py

def factorial(n):
    """Calculate factorial using recursion."""
    if n == 1:
        return 1
    return n * factorial(n - 1)

# How the call stack works for factorial(3):
# - Call factorial(3): Push factorial(3) to the stack
# - Call factorial(2): Push factorial(2) to the stack
# - Call factorial(1): Push factorial(1) to the stack
# - Return from factorial(1): Pop factorial(1) from the stack
# - Return from factorial(2): Pop factorial(2) from the stack
# - Return from factorial(3): Pop factorial(3) from the stack

print(factorial(3))  # Output: 6

In this example, each function call is added to the call stack until the base case is reached. Then, the stack unwinds as each function completes and returns its result. This structured flow is only possible because of the LIFO nature of the call stack.

3. Browser Navigation History

Web browsers use stacks to manage navigation history. When you visit a page, it is added to the history stack. Clicking “Back” pops the last page from the stack, and the browser loads the previous page. Similarly, if you go back and then navigate forward, the forward navigation is handled with a separate stack.

Implementation:

# File: browser_navigation.py
from stack_class import Stack

class BrowserHistory:
    def __init__(self):
        self.history_stack = Stack()
        self.forward_stack = Stack()

    def visit_page(self, url):
        """Visit a new page and add it to the history."""
        self.history_stack.push(url)
        self.forward_stack = Stack()  # Clear forward stack after a new visit

    def back(self):
        """Go back to the previous page."""
        if not self.history_stack.is_empty():
            last_page = self.history_stack.pop()
            self.forward_stack.push(last_page)
            return f"Back to: {self.history_stack.peek() if not self.history_stack.is_empty() else 'Home'}"
        return "No more pages to go back to"

    def forward(self):
        """Go forward to the next page."""
        if not self.forward_stack.is_empty():
            next_page = self.forward_stack.pop()
            self.history_stack.push(next_page)
            return f"Forward to: {next_page}"
        return "No more pages to go forward to"

# Usage example:
browser = BrowserHistory()
browser.visit_page("example.com")
browser.visit_page("openai.com")
print(browser.back())  # Output: Back to: example.com
print(browser.forward())  # Output: Forward to: openai.com

Here, two stacks work together to handle back and forward navigation seamlessly.

Advantages of Using a Stack Class

  • Encapsulation: Encapsulating stack operations in a class ensures all stack-related behavior is managed in one place.
  • Reusability: Once implemented, the stack class can be used across various projects.
  • Flexibility: You can modify the stack’s behavior (e.g., size limits) without changing other parts of your codebase.

Thank you for reading this article. I hope you found it helpful and informative. If you have any questions, or if you would like to suggest new Python code examples or topics for future tutorials, please feel free to reach out. Your feedback and suggestions are always welcome!

Happy coding!
C. C. Python Programming

You can also find this article at Medium.com

Leave a Reply