Flip, Eat, Check β Stack Operations

Table of contents
- π§ What Does a Stack Do?
- π§ Why These Four?
- π₯ Mapping the Analogy: The Pancake Chefβs Workflow
- β οΈ Stack Edge Conditions: Underflow & Overflow
- π¬ Design Principles for Robust Stack Implementation
- π Where Stack Operations Show Up in Real Life
- π§ Python Stack Implementation
- π₯ Challenge 1: Chefβs Assistant β Plate Stack Tracker
- π₯ Challenge 2: Max Stack Height β Enforcing Overflow Logic
- π§ͺ Bonus: Handling Stack Growth (Dynamic Sizing)
- β Recap: What We Built
- π§ Deep Practice Challenges
- β οΈ 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?
- π¬ How Try-Except Works Behind the Scenes
- π Visual Flowchart
- π§ͺ Syntax: Try, Except, Else, Finally
- π§± Custom Exceptions in Stack
- π― Why Use Custom Exceptions?
- β Recap: Key Concepts
- π§ Challenge Exercise

π§ 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:
Operation | Description | Analogy |
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:
Principle | Explanation |
Encapsulation | Only expose push , pop , etc. β never allow direct access to storage |
Constant Time Guarantee | Each operation should run in O(1) |
Predictable Failures | Clearly handle overflow/underflow cases with errors or warnings |
No Index Access | You cannot read or write at arbitrary depth β itβs not a list |
Top-only Access | All 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 whenmax_size
is set.pop()
andpeek()
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
Feature | Code Component | Notes |
Core stack operations | push , pop , peek , size | All in O(1), top-only access |
Safe stack behavior | Error handling for under/overflow | Mimics real-world runtime constraints |
Capacity limit | max_size argument | Optional, enforces bounded model |
Real-world modeling | Kitchen & buffer stacks | Clear tie-ins to OS, hardware, UIs |
π§ Deep Practice Challenges
Custom Exception Classes
DefineStackUnderflowError
andStackOverflowError
to replace standard Python ones.Two Stacks in One Array
Implement two independent stacks sharing the same list from opposite ends.Stack with Min()
Modify the stack to return the minimum element in O(1) time (hint: use an auxiliary stack).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 failexcept
: handle known failureselse
: only runs if no error occurredfinally
: 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?
Advantage | Explanation |
Descriptive error names | Tells you what kind of failure happened |
Separation of concerns | Handle StackOverflowError differently from IndexError |
Reusable and testable | Custom errors can be unit-tested like any other code |
Scalable | Lets you model specific logic for other DSA structures |
β Recap: Key Concepts
Concept | Example / Use |
try-except | Wrap risky logic (e.g., pop from empty stack) |
raise | Signal a problem has occurred |
Custom exceptions | StackUnderflowError , StackOverflowError |
finally | Clean-up, teardown logic (e.g., reset stack) |
Under the hood | Python unwinds the call stack to handle errors |
π§ Challenge Exercise
StackExceptionLogger
Create a logging layer that catches all exceptions and logs:Exception type
Message
Time
Stack size at failure
DSA Error Taxonomy
Create custom errors for:Invalid push (wrong type)
Invalid pop (from frozen stack)
Recursive overflow (simulate)
Trace Annotator
When a custom error occurs, show a mini-traceback usingtraceback
module.
Subscribe to my newsletter
Read articles from gayatri kumar directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by