How to Implement a Stack Using a List in Python
A stack is a linear data structure that follows the LIFO (Last In, First Out) principle: the last element added is the first one to be removed. Think of a stack of plates: you always take the top plate first, and you always place a new plate on top.
Stacks are used extensively in programming for tasks like undo/redo operations, expression evaluation, backtracking algorithms, and function call management (the call stack). In Python, the simplest and most common way to implement a stack is by using a built-in list, since list.append() and list.pop() both operate in O(1) amortized time on the last element.
This guide covers every core stack operation with clear examples and then brings everything together into a clean, reusable class.
Core Stack Operations
Before diving into code, here are the five fundamental operations every stack supports:
| Operation | Description |
|---|---|
| push() | Insert an element onto the top of the stack |
| pop() | Remove and return the top element from the stack |
| top() / peek() | Return the top element without removing it |
| isEmpty() | Check whether the stack is empty |
| size() | Return the number of elements in the stack |
Push: Adding Elements to the Stack
The push operation adds an element to the top of the stack. Since a Python list grows from left to right, the "top" of the stack is the last element in the list. We use the built-in append() method:
stack = []
stack.append(1)
stack.append(2)
stack.append(3)
print("Stack contents:")
for item in stack:
print(item)
Output:
Stack contents:
1
2
3
Element 3 is at the top of the stack because it was added last.
Pop: Removing the Top Element
The pop operation removes and returns the element at the top of the stack. Python's list.pop() (with no arguments) removes the last element, which is exactly what we need:
stack = []
stack.append(1)
stack.append(2)
stack.append(3)
print("Initial stack:", stack)
print("Popped element:", stack.pop())
print("Stack after pop:", stack)
Output:
Initial stack: [1, 2, 3]
Popped element: 3
Stack after pop: [1, 2]
Calling pop() on an empty list raises an IndexError. Always check if the stack is empty before popping:
# This will crash
empty_stack = []
# empty_stack.pop() # raises IndexError: pop from empty list
# A safe approach is to check before popping
def safe_pop(stack):
if len(stack) == 0:
return None
return stack.pop()
Top (Peek): Viewing the Top Element
The top (also called peek) operation returns the element at the top of the stack without removing it. Use index -1 to access the last element in the list:
stack = [1, 2, 3]
def top(stack):
if len(stack) == 0:
return None
return stack[-1]
print("Top element:", top(stack))
print("Stack is unchanged:", stack)
Output:
Top element: 3
Stack is unchanged: [1, 2, 3]
stack[-1] instead of stack[len(stack) - 1]Both expressions return the last element, but stack[-1] is the idiomatic Python way and is more readable:
# This works but is more verbose
top_element = stack[len(stack) - 1]
# This is the Pythonic and cleaner way
top_element = stack[-1]
isEmpty: Checking if the Stack Is Empty
The isEmpty check is essential before performing pop or top operations to avoid errors:
stack = []
def is_empty(stack):
return len(stack) == 0
print(is_empty(stack)) # True
stack.append(10)
print(is_empty(stack)) # False
Output:
True
False
You can also use Python's truthy/falsy evaluation directly:
if not stack:
print("Stack is empty")
Size: Getting the Number of Elements
Use Python's built-in len() function to get the current number of elements:
stack = [1, 2, 3]
print("Stack size:", len(stack))
Output:
Stack size: 3
Complete Stack Class Implementation
While using standalone functions works for quick scripts, wrapping everything in a class provides better encapsulation, reusability, and a cleaner API. Here is a complete, well-structured Stack class:
class Stack:
def __init__(self):
self._items = []
def push(self, data):
"""Add an element to the top of the stack."""
self._items.append(data)
def pop(self):
"""Remove and return the top element. Returns None if empty."""
if self.is_empty():
print("Stack is empty; nothing to pop")
return None
return self._items.pop()
def top(self):
"""Return the top element without removing it. Returns None if empty."""
if self.is_empty():
return None
return self._items[-1]
def is_empty(self):
"""Return True if the stack has no elements."""
return len(self._items) == 0
def size(self):
"""Return the number of elements in the stack."""
return len(self._items)
def display(self):
"""Print the stack contents from bottom to top."""
if self.is_empty():
print("Stack is empty")
else:
print("Stack (bottom -> top):", self._items)
def __str__(self):
return str(self._items)
Example Usage and Output
s = Stack()
# Push elements
s.push("Alice")
s.push("Bob")
s.push("Charlie")
s.push("Diana")
s.display()
print("Size:", s.size())
# Pop the top element
print("Popped:", s.pop())
# Peek at the new top
print("Top element:", s.top())
# Check if empty
print("Is empty?", s.is_empty())
# Push a new element
s.push("Eve")
s.display()
Output:
Stack (bottom -> top): ['Alice', 'Bob', 'Charlie', 'Diana']
Size: 4
Popped: Diana
Top element: Charlie
Is empty? False
Stack (bottom -> top): ['Alice', 'Bob', 'Charlie', 'Eve']
Step-by-step explanation:
- Four names are pushed onto the stack.
"Diana"is at the top. size()returns4.pop()removes and returns"Diana", the most recently added element.top()returns"Charlie", the new top element, without removing it.is_empty()returnsFalsebecause three elements remain."Eve"is pushed, becoming the new top element.
Common Mistake: Passing the List as a Parameter
A design flaw seen in many beginner implementations is passing the internal list as an argument to every method:
# A poor design where the list is external to the class
class BadStack:
def push(self, st, data):
st.append(data)
def pop(self, st):
return st.pop()
external_list = []
s = BadStack()
s.push(external_list, 10)
This defeats the purpose of using a class because the data is not encapsulated. Anyone can modify external_list directly, breaking the stack's integrity.
# A correct design where the list is managed internally
class Stack:
def __init__(self):
self._items = []
def push(self, data):
self._items.append(data)
def pop(self):
if self.is_empty():
return None
return self._items.pop()
The stack owns its data. External code interacts only through the defined methods.
Time and Space Complexity
| Operation | Time Complexity | Explanation |
|---|---|---|
| push() | O(1) amortized | list.append() is O(1) amortized; occasional resizing is O(n) but averages out |
| pop() | O(1) | Removing the last element requires no shifting |
| top() | O(1) | Accessing list[-1] is a direct index lookup |
| is_empty() | O(1) | len() on a list is a constant-time operation |
| size() | O(1) | len() on a list is a constant-time operation |
Space Complexity: O(n), where n is the number of elements stored in the stack.
list.pop() is O(1) but list.pop(0) is O(n)Python's list.pop() without arguments removes the last element, which requires no shifting. However, list.pop(0) removes the first element and shifts all others left, making it O(n). This is why lists work perfectly for stacks (LIFO) but are inefficient for queues (FIFO) without a circular buffer or collections.deque.
Conclusion
Python's built-in list is an excellent choice for implementing a stack thanks to its O(1) amortized append() and O(1) pop() operations.
- By wrapping these operations in a well-designed class, you get a clean, reusable stack with proper encapsulation and safety checks.
- This implementation is suitable for learning, coding interviews, and most practical applications.
- For thread-safe scenarios, consider using
queue.LifoQueuefrom Python's standard library instead.