Skip to main content

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:

OperationDescription
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]
Popping from an empty stack raises an error

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]
Use 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:

  1. Four names are pushed onto the stack. "Diana" is at the top.
  2. size() returns 4.
  3. pop() removes and returns "Diana", the most recently added element.
  4. top() returns "Charlie", the new top element, without removing it.
  5. is_empty() returns False because three elements remain.
  6. "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

OperationTime ComplexityExplanation
push()O(1) amortizedlist.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.

Why 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.LifoQueue from Python's standard library instead.