← Back to Learning Hub

Lesson 4: OOP Data Structures

Building Stack & Queue Classes from Scratch

90 minutes Data Structures LIFO/FIFO

Today's Goal

Reinforce your OOP skills while learning two fundamental data structures: Stack and Queue. By the end, you'll be able to add an UNDO feature to your shop system!

Connection: You already understand the Stack concept ("like dishes"). Today we'll learn the technical terminology and implement it as a class.

Part 1: Quick Review 10 min

Lesson 3 Recap

Quick questions to warm up:

Review Questions

  • CIA Triad: What are the three fundamental principles? (Name all three)
  • Code Quality: What are the three qualities of good code?
  • Exceptions: Name the three exception types we covered.
  • Scenario: What exception type is raised when you access a missing dictionary key?

Answers (Click to Reveal)

CIA Triad

Confidentiality, Integrity, Availability

Code Quality Principles

Readability, Maintainability, Robustness

Exception Types

ValueError, KeyError, TypeError

Missing Dictionary Key

KeyError

Part 2: Homework Check 25 min

Enhancement Challenges Review

Let's review your implementation of the challenges from Lesson 3.

What to Check

Challenge What to Look For
A: Error Handling try/except for KeyError, friendly error messages
B: Case Insensitive .title() or .lower() approach for input
C: Shopping Loop while loop, exit condition, total tracking
D: Input Validation Negative budget check, ValueError handling
E: Better Display Formatted items, number selection option

Transition: "Now let's add an UNDO button to your shop system - to do this, we need to learn about Stacks."

Part 3: LIFO & FIFO Introduction 10 min

Stack: LIFO (Last In, First Out)

The last item added is the first one removed.

Real-World Examples

  • Stack of dishes - You wash the one on top first (the last one placed)
  • Browser back button - Goes to the most recently visited page
  • Ctrl+Z (Undo) - Undoes the most recent action
  • Function call stack - Most recently called function returns first

Queue: FIFO (First In, First Out)

The first item added is the first one removed.

Real-World Examples

  • Drive-through queue - First car in line gets served first
  • Printer jobs - First document sent prints first
  • Customer service line - First person waiting gets helped first
  • Message queue - Messages processed in order received

Terminology

Operation Stack Queue
Add item push() enqueue()
Remove item pop() dequeue()
View next item (without removing) peek() front()

Memory trick: LIFO = "Last In, First Out" (Stack) | FIFO = "First In, First Out" (Queue)

Why OOP for Data Structures? 5 min

Benefits of Using Classes

Why not just use a Python list directly? Here's why OOP makes sense:

1. Encapsulation

Hide the implementation details. Users of your Stack don't need to know it uses a list internally.

2. Controlled Behavior

Prevent misuse. A Stack should only allow push/pop - not random access to the middle!

3. Clear Interface

stack.push(item) is clearer than list.append(item) when you're thinking about stacks.

4. Error Handling

The class can handle edge cases (like popping from an empty stack) gracefully.

Connection to Lesson 2: Remember encapsulation? This is it in action - hiding the internal list and only exposing stack operations.

Building the Stack Class 15 min

Hands-On: Code This Together

Blake codes while instructor guides. Let's build a complete Stack class.

Stack Class Implementation
class Stack:
    def __init__(self):
        self._items = []  # Private - encapsulation!

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

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

    def peek(self):
        """Return the top item without removing it."""
        if self.is_empty():
            raise IndexError("Cannot peek at empty stack")
        return self._items[-1]

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

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

Discussion Points

  • Why _items with underscore? - Private convention. Signals "don't access directly!"
  • Why raise IndexError? - Robustness from Lesson 3! Prevents None bugs.
  • Why is_empty() method? - Clearer than checking len(stack._items) == 0
Testing the Stack
# Create a stack
my_stack = Stack()

# Push some items
my_stack.push("A")
my_stack.push("B")
my_stack.push("C")

print(f"Size: {my_stack.size()}")      # 3
print(f"Top item: {my_stack.peek()}")  # C

# Pop items (LIFO order!)
print(my_stack.pop())  # C (last in)
print(my_stack.pop())  # B
print(my_stack.pop())  # A (first in, last out)

# This would raise IndexError:
# my_stack.pop()  # Empty stack!

Practical Application: Undo Feature 10 min

Connecting to Your Shop System

Let's create an UndoStack that records actions and can undo them!

UndoStack Class
class UndoStack:
    def __init__(self):
        self._actions = []

    def record_action(self, description, data):
        """Record an action that can be undone."""
        self._actions.append({
            'description': description,
            'data': data
        })
        print(f"Recorded: {description}")

    def undo(self):
        """Undo the last action. Returns the action data or None."""
        if not self._actions:
            print("Nothing to undo!")
            return None
        action = self._actions.pop()
        print(f"Undoing: {action['description']}")
        return action['data']

    def can_undo(self):
        """Check if there are actions to undo."""
        return len(self._actions) > 0

    def show_history(self):
        """Display all recorded actions."""
        if not self._actions:
            print("No actions recorded")
            return
        print("Action history:")
        for i, action in enumerate(self._actions, 1):
            print(f"  {i}. {action['description']}")
Using UndoStack with Purchases
# Demo: Recording and undoing purchases
undo_stack = UndoStack()
budget = 100

# Simulate purchases
def purchase(item, price):
    global budget
    budget -= price
    undo_stack.record_action(
        f"Purchased {item} for ${price}",
        {'item': item, 'price': price}
    )
    print(f"Budget: ${budget}")

# Make some purchases
purchase("Apple", 1)      # Budget: $99
purchase("Watch", 50)     # Budget: $49
purchase("Toothpaste", 3) # Budget: $46

# Show what we can undo
undo_stack.show_history()

# Undo last purchase
action = undo_stack.undo()  # Undoing: Purchased Toothpaste for $3
if action:
    budget += action['price']  # Restore the money
    print(f"Budget restored to: ${budget}")  # $49

Why This Works

  • Stack stores actions in order (LIFO)
  • Undo always reverses the most recent action
  • Storing data with each action lets us reverse it

Quick Queue Introduction 5 min

Queue Class Structure

A Queue follows FIFO - first in, first out. Here's what the structure looks like:

Queue Class Implementation
class Queue:
    def __init__(self):
        self._items = []

    def enqueue(self, item):
        """Add an item to the back of the queue."""
        self._items.append(item)

    def dequeue(self):
        """Remove and return the front item."""
        if self.is_empty():
            raise IndexError("Cannot dequeue from empty queue")
        return self._items.pop(0)  # Remove from front!

    def front(self):
        """Return the front item without removing it."""
        if self.is_empty():
            raise IndexError("Cannot peek at empty queue")
        return self._items[0]

    def is_empty(self):
        return len(self._items) == 0

    def size(self):
        return len(self._items)
Queue Demo: Order Processing
# Order processing queue
order_queue = Queue()

# Orders come in
order_queue.enqueue("Order #101 - Pizza")
order_queue.enqueue("Order #102 - Burger")
order_queue.enqueue("Order #103 - Salad")

# Process orders (FIFO - first order gets processed first)
print(order_queue.dequeue())  # Order #101 - Pizza
print(order_queue.dequeue())  # Order #102 - Burger

# What's next?
print(f"Next order: {order_queue.front()}")  # Order #103 - Salad

Key Difference: Stack uses pop() (removes from end), Queue uses pop(0) (removes from front).

Comparison & Summary 5 min

Stack vs Queue Comparison

Feature Stack Queue
Principle LIFO (Last In, First Out) FIFO (First In, First Out)
Add operation push() enqueue()
Remove operation pop() dequeue()
Analogy Stack of dishes Checkout line
Use case Undo, browser history Order processing, print queue

Quick Quiz

  • Add A, B, C to a Stack, then pop twice. What's left?
  • Add A, B, C to a Queue, then dequeue twice. What's left?

Answers (Click to Reveal)

Stack Answer

Push A, B, C → Stack is [A, B, C] (C on top)

Pop → removes C → [A, B]

Pop → removes B → [A]

Answer: A is left

Queue Answer

Enqueue A, B, C → Queue is [A, B, C] (A at front)

Dequeue → removes A → [B, C]

Dequeue → removes B → [C]

Answer: C is left

Homework Options Choose 1-2

Choose Your Challenge

Pick 1-2 options to practice at home:

Option A: Integrate Undo into Shop System

Add an undo feature to your shop system from Lesson 3:

  • Record each purchase using UndoStack
  • Add an "undo last purchase" menu option
  • Restore the budget when undoing
  • Handle trying to undo when no purchases made
Option B: Order Queue System

Build a simple order management system:

  • Menu: Add order, Process next order, Show pending
  • Use a Queue to manage orders
  • Track completed orders separately
  • Show order position in queue
Option C: Browser History Simulator

Create a simple browser history using Stack:

  • visit(url) - push URL to history stack
  • back() - pop and return to previous URL
  • current() - show current page (peek)
  • Handle edge case: can't go back on first page

What You've Learned

  • LIFO - Last In, First Out (Stack)
  • FIFO - First In, First Out (Queue)
  • Stack operations: push, pop, peek
  • Queue operations: enqueue, dequeue, front
  • Why OOP is useful for data structures (encapsulation)
  • Practical application: Undo functionality

Gap Filled!

  • You now know LIFO/FIFO terminology (previously identified as a gap)
  • You can implement and use Stack and Queue data structures

Next lesson: We'll continue building on OOP concepts and explore more Year 12 topics.

← Back to Blake's Learning Hub