ScreenshotNeo

BlogEngineering

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.

By the ScreenshotNeo team30 September 20268 min read

Understanding Stack Implementation in Python

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:

A stack removes the newest item first: last in, first out.
A stack removes the newest item first: last in, first out.
  • 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.

Lists are straightforward for one-ended stacks; deques support efficient operations at both ends.
Lists are straightforward for one-ended stacks; deques support efficient operations at both ends.
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] raises IndexError. Define whether your API raises, returns None, or returns a result object.
  • Falsy values: values such as 0, False, and None are 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.LifoQueue when 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.