Flip, Eat, Check – Stack Operations

gayatri kumargayatri kumar
10 min read

🧭 What Does a Stack Do?

In our previous article, we explored the stack as a concept β€” a structure where the last element added is the first to be removed (LIFO). But what defines a stack in terms of operations?

The behavior of a stack is not its form, but its strict interface.

There are four core operations:

OperationDescriptionAnalogy
push(x)Insert item x at the top of the stack🍳 Place new pancake
pop()Remove and return the top item🍽️ Eat the top pancake
peek()Return the top item without removing itπŸ‘€ Check which pancake is on top
size()Return the total number of items in the stackπŸ“ Count how many pancakes

🧠 Why These Four?

Let’s look at it from an API design perspective:

  • These operations define a complete but minimal interface.

  • No operation breaks the stack's LIFO contract.

  • They support constant-time operations β€” each one should run in O(1) time.

  • No indexing, searching, or mutation outside the top is permitted.

This makes stacks predictable, efficient, and easy to reason about, which is why they’re foundational in recursion, compiler design, and backtracking algorithms.


πŸ₯ž Mapping the Analogy: The Pancake Chef’s Workflow

Let’s formalize the analogy. The chef uses a plate to serve pancakes one by one:

  • Push: Add a pancake to the plate pile.

  • Pop: Remove the top pancake for serving.

  • Peek: Check the top pancake without touching the rest.

  • Size: Count how many pancakes are waiting to be served.

Stack State:

Time    Operation          Stack (Top β†’ Bottom)
──────  ───────────────    ───────────────────────
T1      push("Banana")     [ Banana ]
T2      push("Blueberry")  [ Blueberry, Banana ]
T3      push("Choco")      [ Choco, Blueberry, Banana ]
T4      peek()             Top is: Choco
T5      pop()              Remove: Choco
T6      size()             2 pancakes remain

Just like in a real kitchen, the chef can’t pull the second pancake from the middle of the stack β€” that would cause a mess.


⚠️ Stack Edge Conditions: Underflow & Overflow

🧊 Stack Underflow

Occurs when you try to pop() or peek() from an empty stack.

Real analogy: The chef tries to serve a pancake when the plate is empty. Result? ❌

Stack: []
Action: pop() β†’ Error: Stack is empty

πŸ”₯ Stack Overflow

Occurs when you try to push() onto a stack that’s reached its maximum size (if one is defined).

Real analogy: The chef stacks too many pancakes β€” the plate collapses.

Capacity = 3
Stack: [ Choco, Blueberry, Banana ]
Action: push("Strawberry") β†’ Error: Stack is full

These edge cases must be handled gracefully in both software and real systems. For instance:

  • In C/C++, stack overflows can crash the program (e.g., infinite recursion).

  • In Java, StackOverflowError is a JVM-thrown fatal error.

  • In interpreters and parsers, stack underflow is used to detect syntax mismatch.


πŸ”¬ Design Principles for Robust Stack Implementation

Here are a few rules and mental models we follow while implementing stack operations:

PrincipleExplanation
EncapsulationOnly expose push, pop, etc. β€” never allow direct access to storage
Constant Time GuaranteeEach operation should run in O(1)
Predictable FailuresClearly handle overflow/underflow cases with errors or warnings
No Index AccessYou cannot read or write at arbitrary depth β€” it’s not a list
Top-only AccessAll logic should go through the stack’s top β€” enforce strict LIFO rules

πŸ”„ Where Stack Operations Show Up in Real Life

🧭 Browser Back Button

Each page visited is push()ed. Clicking back is a pop(). peek() would show the current page. If you're on the first page, pop() triggers an "empty history" edge case.

🧾 Command Line History

Your terminal stores commands in a stack. When you press the up arrow, it’s like doing a reverse pop() from the stack of previously entered commands.

πŸ“ž Call Stack in Programs

Each function call is push()ed. When a function returns, it's pop()ed. Errors like "maximum recursion depth exceeded" in Python are stack overflows.


πŸ”§ Python Stack Implementation

We'll now write a complete Stack class that:

  • Mimics system stack behavior

  • Enforces access rules (top-only)

  • Handles underflow & overflow clearly

  • Provides inspection operations (peek, size, etc.)

🧱 The Code

class Stack:
    def __init__(self, max_size=None):
        """
        Initialize a stack.
        :param max_size: Optional integer for maximum capacity.
        """
        self._items = []           # Internal list to store elements
        self._max_size = max_size  # Optional capacity limit

    def push(self, item):
        """
        Add an item to the top of the stack.
        Raises OverflowError if max_size is reached.
        """
        if self._max_size is not None and len(self._items) >= self._max_size:
            raise OverflowError("Stack Overflow: Maximum capacity reached.")
        self._items.append(item)

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

    def peek(self):
        """
        Return the top item without removing it.
        Returns None if stack is empty.
        """
        return self._items[-1] if not self.is_empty() else None

    def is_empty(self):
        """
        Check if the stack has no elements.
        """
        return len(self._items) == 0

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

    def __repr__(self):
        return f"Top β†’ {list(reversed(self._items))}"

πŸ” Internal Logic Recap

  • self._items is a private list, never accessed directly outside the class β€” this maintains encapsulation.

  • push() guards against overflow when max_size is set.

  • pop() and peek() guard against underflow with clear error messaging.

  • All core operations are O(1) due to list tail efficiency.

This implementation respects stack design principles:

  • Strict top-only access

  • Constant time ops

  • Boundary awareness


πŸ₯„ Challenge 1: Chef’s Assistant – Plate Stack Tracker

🎯 Task:

Simulate a stack of plates in a kitchen. The assistant can:

  • Add clean plates to the stack.

  • Remove plates to serve.

  • Inspect the top plate.

  • Print how many plates are available.

plates = Stack()

# Add clean plates
plates.push("Plate #1")
plates.push("Plate #2")
plates.push("Plate #3")

print("πŸ“¦ Current Plates:", plates)

# Peek at the top plate
print("πŸ‘€ Top plate:", plates.peek())

# Remove a plate to serve
served = plates.pop()
print("🍽️ Served:", served)

# Check stack size
print("πŸ“ Plates remaining:", plates.size())

Output:

πŸ“¦ Current Plates: Top β†’ ['Plate #3', 'Plate #2', 'Plate #1']
πŸ‘€ Top plate: Plate #3
🍽️ Served: Plate #3
πŸ“ Plates remaining: 2

🧠 Real-World Model

This challenge directly reflects:

  • Inventory systems: Topmost items go first (e.g., push pallet boxes).

  • Tool trays / plate dispensers: You remove from the top only.

  • System-level stacks: Where most recent state is used first.


πŸ₯ž Challenge 2: Max Stack Height – Enforcing Overflow Logic

🎯 Task:

Create a plate stack with limited height (max_size) and prevent over-stacking.

max_plates = 3
dish_stack = Stack(max_size=max_plates)

try:
    for i in range(1, 6):  # Intentionally trying to push more than capacity
        dish_stack.push(f"Plate #{i}")
        print(f"βœ… Added: Plate #{i}")
except OverflowError as e:
    print("❌", e)

print("Final Stack:", dish_stack)

Output:

βœ… Added: Plate #1
βœ… Added: Plate #2
βœ… Added: Plate #3
❌ Stack Overflow: Maximum capacity reached.
Final Stack: Top β†’ ['Plate #3', 'Plate #2', 'Plate #1']

🧠 Real-World Model

This is how bounded buffers work:

  • In network routers, packet stacks have a cap β€” excess packets are dropped or queued elsewhere.

  • In operating systems, thread stacks have strict size limits β€” overflow causes crashes.


πŸ§ͺ Bonus: Handling Stack Growth (Dynamic Sizing)

In production systems:

  • You often monitor the stack depth.

  • In Python, you can use len(stack._items) to check current size.

  • Dynamic stacks can be resized (e.g., doubling array size on overflow), but we skipped this here for clarity.


βœ… Recap: What We Built

FeatureCode ComponentNotes
Core stack operationspush, pop, peek, sizeAll in O(1), top-only access
Safe stack behaviorError handling for under/overflowMimics real-world runtime constraints
Capacity limitmax_size argumentOptional, enforces bounded model
Real-world modelingKitchen & buffer stacksClear tie-ins to OS, hardware, UIs

🧠 Deep Practice Challenges

  1. Custom Exception Classes
    Define StackUnderflowError and StackOverflowError to replace standard Python ones.

  2. Two Stacks in One Array
    Implement two independent stacks sharing the same list from opposite ends.

  3. Stack with Min()
    Modify the stack to return the minimum element in O(1) time (hint: use an auxiliary stack).

  4. Reverse Polish Notation Evaluator
    Evaluate expressions like ["3", "4", "+", "2", "*"] using stack logic.


⚠️ Exception Handling in Python – Try, Except, and Custom Errors

A deep dive into system-safe logic for our stack data structure

🧭 Why Do We Need Exceptions?

Let’s say you try to pop from an empty stack:

stack = Stack()
stack.pop()  # ❌ What happens?

This is an illegal operation. You have two options:

❌ Option 1: Return None or a Special Value

def pop(self):
    if self.is_empty():
        return None

But this silently hides the error, which could cause bugs downstream (e.g., comparing None > 5).

βœ… Option 2: Raise an Exception

def pop(self):
    if self.is_empty():
        raise IndexError("Stack Underflow")

This immediately halts the program unless handled β€” making the bug explicit.

πŸ’‘ Exceptions give us flow control for abnormal states, keeping the system predictable and safe.


πŸ”¬ How Try-Except Works Behind the Scenes

When Python runs a try block, it monitors each instruction for exceptions. If one occurs:

  • Python looks up the exception type.

  • It unwinds the call stack to find a matching except.

  • If found, it jumps to that block and continues.

  • If not, the program crashes with a traceback.


🧠 Stack Trace (Call Stack) View

def A():
    B()

def B():
    C()

def C():
    raise ValueError("Oops")

If unhandled:

Traceback (most recent call last):
  File "main.py", line 10, in <module>
    A()
  File "main.py", line 2, in A
    B()
  File "main.py", line 5, in B
    C()
  File "main.py", line 8, in C
    raise ValueError("Oops")
ValueError: Oops

πŸ” Python walks backward through the stack to find an error handler.


πŸ“Š Visual Flowchart

try:
β”‚
β”œβ”€βœ” if no error β†’ run code normally
β”‚
β””β”€βœ– if error β†’ jump to matching except block
       β”‚
       β”œβ”€ if match found β†’ run handler
       β”‚
       └─ if no match β†’ unwind call stack β†’ print traceback

πŸ§ͺ Syntax: Try, Except, Else, Finally

try:
    result = 10 / 0
except ZeroDivisionError:
    print("❌ Cannot divide by zero")
else:
    print("βœ… Success!")
finally:
    print("πŸ“¦ Clean-up complete.")

Block Use-Cases:

  • try: code that might fail

  • except: handle known failures

  • else: only runs if no error occurred

  • finally: always runs β€” use for clean-up (closing files, releasing memory)


🧱 Custom Exceptions in Stack

Step 1: Define Your Own Exception Class

class StackUnderflowError(Exception):
    """Raised when popping from an empty stack."""
    pass

class StackOverflowError(Exception):
    """Raised when pushing to a full stack."""
    pass

These inherit from Python’s Exception base class β€” making them compatible with try-except.

Step 2: Use Them in Your Stack

class Stack:
    def __init__(self, max_size=None):
        self._items = []
        self._max_size = max_size

    def push(self, item):
        if self._max_size is not None and len(self._items) >= self._max_size:
            raise StackOverflowError("Stack Overflow: Max size reached.")
        self._items.append(item)

    def pop(self):
        if self.is_empty():
            raise StackUnderflowError("Stack Underflow: Nothing to pop.")
        return self._items.pop()

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

Step 3: Handling Errors in Client Code

stack = Stack(max_size=2)

try:
    stack.push("A")
    stack.push("B")
    stack.push("C")  # Exceeds capacity
except StackOverflowError as e:
    print("⚠️", e)

try:
    empty_stack = Stack()
    empty_stack.pop()
except StackUnderflowError as e:
    print("⚠️", e)

🎯 Why Use Custom Exceptions?

AdvantageExplanation
Descriptive error namesTells you what kind of failure happened
Separation of concernsHandle StackOverflowError differently from IndexError
Reusable and testableCustom errors can be unit-tested like any other code
ScalableLets you model specific logic for other DSA structures

βœ… Recap: Key Concepts

ConceptExample / Use
try-exceptWrap risky logic (e.g., pop from empty stack)
raiseSignal a problem has occurred
Custom exceptionsStackUnderflowError, StackOverflowError
finallyClean-up, teardown logic (e.g., reset stack)
Under the hoodPython unwinds the call stack to handle errors

🧠 Challenge Exercise

  1. StackExceptionLogger
    Create a logging layer that catches all exceptions and logs:

    • Exception type

    • Message

    • Time

    • Stack size at failure

  2. DSA Error Taxonomy
    Create custom errors for:

    • Invalid push (wrong type)

    • Invalid pop (from frozen stack)

    • Recursive overflow (simulate)

  3. Trace Annotator
    When a custom error occurs, show a mini-traceback using traceback module.

0
Subscribe to my newsletter

Read articles from gayatri kumar directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

gayatri kumar
gayatri kumar