Understanding Stack Implementation in Python
Learn how to implement a LIFO stack in Python with lists, deque, custom classes, complexity guidance, edge-case handling, and runnable examples.

A stack is a last-in, first-out (LIFO) data structure: the most recently added item is the first one removed. In Python, the simplest implementation is a list with append() for push and pop() for pop. Keep the stack top at the right-hand end of the list.
stack = []
stack.append("first") # push
stack.append("second") # push
item = stack.pop() # returns "second"
print(item)
Python’s tutorial explicitly recommends lists for stack behavior, and the final-element operations are constant time in CPython. This guide covers list and collections.deque choices, a reusable class, complexity, empty-stack behavior, testing, performance, and production concerns.
1. What a stack does
A stack exposes two core operations:

- Push: add an item to the top.
- Pop: remove and return the item at the top.
A useful companion operation is peek, which reads the top item without removing it. An empty check and a length query are also common.
stack = []
stack.append("page A")
stack.append("page B")
stack.append("page C")
print(stack[-1]) # peek: page C
print(stack.pop()) # page C
print(stack.pop()) # page B
print(stack) # ['page A']
The order is LIFO: page C leaves before page B, and page B leaves before page A. The Python documentation describes this exact list usage in its section Using Lists as Stacks.
2. Implementing a stack with a list
Use a list when all stack operations happen at one end. The right-hand end is the top because append() adds there and pop() with no index removes there.
def demo_list_stack():
stack = []
# Push values
for value in (10, 20, 30):
stack.append(value)
# Pop until empty
while stack:
print(stack.pop())
demo_list_stack()
Expected output is 30, 20, then 10. Checking while stack prevents a removal from an empty list.
Checking the top safely
if stack:
top = stack[-1]
else:
top = None
Indexing with stack[-1] raises IndexError when the list is empty. Decide whether that exception or a sentinel such as None makes more sense for your application.
Do not use the left end
# Avoid this for a stack:
stack.insert(0, value)
value = stack.pop(0)
Inserting or removing at index zero moves the remaining elements, making each operation O(n). The Python time-complexity reference records append as O(1) and pop(k) as O(n-k); therefore pop() at the final index is O(1). These figures describe CPython built-in types and can differ in other Python implementations.
3. Using collections.deque
collections.deque is a double-ended queue. It supports efficient operations at both ends and is a good choice when your design may need appendleft() or popleft(), or when a double-ended API communicates intent clearly.

from collections import deque
stack = deque()
stack.append("first")
stack.append("second")
print(stack.pop()) # second
print(stack[-1]) # peek at the remaining top
The standard-library documentation lists append, appendleft, pop, and popleft for deques. For a stack that only pushes and pops on the right, a list is usually the clearest choice. Choose a deque when both ends matter or when you may later evolve the structure into a queue.
| Question | List | Deque |
|---|---|---|
| Push and pop on right | Excellent | Excellent |
| Operations on left | O(n) for removal/insertion | Designed for efficient end operations |
| Random indexing | Natural and efficient | Supported, but middle access is slower |
| Simple stack code | Usually clearest | Useful when both ends may be needed |
| Bounded history | Requires manual trimming | Can use maxlen |
from collections import deque
recent = deque(maxlen=3)
for event in ("a", "b", "c", "d"):
recent.append(event)
print(recent) # deque(['b', 'c', 'd'], maxlen=3)
4. Building a reusable Stack class
A wrapper keeps the container private and gives callers a stable API. This is useful when you need validation, domain-specific errors, logging, or a future change from list to deque.
class Stack:
def __init__(self):
self._items = []
def push(self, value):
self._items.append(value)
def pop(self):
return self._items.pop()
def peek(self):
return self._items[-1]
def is_empty(self):
return not self._items
def __len__(self):
return len(self._items)
s = Stack()
s.push("compile")
s.push("test")
print(s.peek()) # test
print(s.pop()) # test
print(len(s)) # 1
print(s.is_empty()) # False
This class deliberately preserves Python’s normal IndexError behavior for pop() and peek() on an empty stack. That is a design choice, not a requirement.
Returning a domain-specific error
class EmptyStackError(Exception):
"""Raised when a stack operation needs an item but none exists."""
class SafeStack:
def __init__(self):
self._items = []
def push(self, value):
self._items.append(value)
def pop(self):
if not self._items:
raise EmptyStackError("cannot pop from an empty stack")
return self._items.pop()
def peek(self):
if not self._items:
raise EmptyStackError("cannot peek at an empty stack")
return self._items[-1]
Use a custom exception when callers need to distinguish an empty-stack condition from unrelated IndexError failures. If an empty result is valid, provide pop_or_none() or use an explicit optional return type instead of silently hiding errors.
5. Type hints and a generic stack
Type hints make the intended value type clear to readers and static analyzers. A generic class can hold any type while retaining checking.
from typing import Generic, TypeVar
T = TypeVar("T")
class TypedStack(Generic[T]):
def __init__(self):
self._items: list[T] = []
def push(self, value: T) -> None:
self._items.append(value)
def pop(self) -> T:
return self._items.pop()
def peek(self) -> T:
return self._items[-1]
def __len__(self) -> int:
return len(self._items)
numbers = TypedStack[int]()
numbers.push(1)
numbers.push(2)
print(numbers.pop())
6. Complexity, memory, and performance
| Operation | List at right end | List at left end | Deque at either end |
|---|---|---|---|
| Push | Amortized O(1) | O(n) | O(1) |
| Pop | O(1) | O(n) | O(1) |
| Peek at right | O(1) | Not applicable | O(1) |
| Length | O(1) | O(1) | O(1) |
“Amortized” means occasional list resizing is spread across many appends; the long-run average remains constant time. Both containers store references to objects, so the memory cost includes the container’s slots plus the objects referenced. Popping removes a reference from the container, but another reference elsewhere can keep the object alive.
For a bounded stack, enforce a limit explicitly:
class BoundedStack:
def __init__(self, capacity):
if capacity < 1:
raise ValueError("capacity must be positive")
self._items = []
self._capacity = capacity
def push(self, value):
if len(self._items) >= self._capacity:
raise OverflowError("stack capacity exceeded")
self._items.append(value)
def pop(self):
return self._items.pop()
7. Empty stacks and other edge cases
- Pop when empty: list and deque raise
IndexError. Catch it or check truthiness first. - Peek when empty:
items[-1]raisesIndexError. Define whether your API raises, returnsNone, or returns a result object. - Falsy values: values such as
0,False, andNoneare valid stack entries. Check the container, not the top value. - Mutability: the stack stores references. Mutating a pushed list or dictionary changes the same object visible after popping.
- Iteration: iterating a list runs from bottom to top. To inspect pop order without modifying it, use
reversed(stack). - Concurrency: a check followed by a pop is not an atomic application-level transaction. Protect compound operations with a lock, or use
queue.LifoQueuewhen you need a thread-safe LIFO queue.
8. Practical stack patterns
Undo history
undo = []
undo.append(("title", "old value"))
undo.append(("color", "blue"))
field, previous = undo.pop()
Depth-first traversal
def depth_first(graph, start):
seen = set()
stack = [start]
order = []
while stack:
node = stack.pop()
if node in seen:
continue
seen.add(node)
order.append(node)
stack.extend(reversed(graph.get(node, [])))
return order
Matching delimiters
def balanced(text):
pairs = {")": "(", "]": "[", "}": "{"}
opens = set(pairs.values())
stack = []
for char in text:
if char in opens:
stack.append(char)
elif char in pairs:
if not stack or stack.pop() != pairs[char]:
return False
return not stack
9. Testing a stack implementation
Test behavior at the API boundary, especially empty operations and LIFO order.
def test_stack():
s = Stack()
assert s.is_empty()
s.push("a")
s.push("b")
assert len(s) == 2
assert s.peek() == "b"
assert s.pop() == "b"
assert s.pop() == "a"
assert s.is_empty()
try:
s.pop()
except IndexError:
pass
else:
raise AssertionError("empty pop should fail")
test_stack()
10. Troubleshooting common mistakes
| Symptom | Cause | Fix |
|---|---|---|
| Items come out in insertion order | You are removing index zero or using a queue | Push and pop at the same end, normally append()/pop() |
IndexError: pop from empty list |
Pop ran before an item was pushed | Check if stack, catch the exception, or define a custom empty error |
IndexError from peek |
stack[-1] was used while empty |
Check emptiness before indexing |
| Slow performance on large input | pop(0) or insert(0, ...) |
Use the right end, or use deque for both ends |
| Unexpected changes to stored data | A mutable object was stored by reference | Copy the object when isolation is required |
| Stack grows without bound | No capacity or retention policy | Set a limit, trim history, or use deque(maxlen=...) |
11. Or skip the browser setup
If your application needs screenshots of stack visualizations, documentation pages, or generated results, ScreenshotNeo provides a single HTTP request that returns PNG, JPEG, WebP, or PDF. It accepts cookie banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers report the page verdict and billing status.
See the ScreenshotNeo API documentation for all options, including full-page capture, CSS-element capture, device presets, dark mode, custom CSS and JavaScript, waits, request blocking, headers, cookies, geolocation, caching, signed links, asynchronous jobs, bulk capture, and PDF settings.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://docs.python.org/3/tutorial/datastructures.html \
-o stack-guide.webp
Python
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={
"access_key": "YOUR_API_KEY",
"url": "https://docs.python.org/3/tutorial/datastructures.html",
},
timeout=90,
)
r.raise_for_status()
open("stack-guide.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://docs.python.org/3/tutorial/datastructures.html'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('stack-guide.webp', Buffer.from(await res.arrayBuffer()));
Free accounts include 1,000 screenshots each month with no card. Paid plans start at $5 for 3,000 shots, and every feature is available on every plan. An MCP server exposes take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. Create a free ScreenshotNeo account.
12. Frequently asked questions
Should I use a list or deque for a stack?
Use a list for ordinary one-ended push and pop. Use a deque when you need efficient operations at both ends or a bounded history.
Is Python’s list stack thread-safe?
Individual operations may be atomic under some interpreter implementations, but a multi-step check-and-pop sequence is not a synchronization policy. Use a lock or queue.LifoQueue for coordinated worker threads.
Can a stack contain duplicate values?
Yes. A stack tracks positions, so duplicate values are independent entries.
How do I inspect a stack without destroying it?
Use indexing for the top, reversed(stack) for pop order, or copy it before experimentation.
When should I write a custom class?
Write one when you need a restricted public API, validation, custom exceptions, instrumentation, or a future storage change.


