Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use a Python list as a stack when you need last-in, first-out (LIFO) behavior at one end: call append() to push and pop() with no index to remove the top item. In CPython, both operations at the list’s right-hand end are O(1). Choose collections.deque instead when you also need efficient operations at the left end or a deliberately double-ended API.

What a stack means in Python

A stack is an abstract data type in which the most recently added item is the first one removed. This is called last-in, first-out (LIFO). A useful physical model is a pile of plates: you place a plate on top and take the top plate off before touching anything underneath.

Python’s official tutorial describes lists as an easy way to use a stack: the last element added is the first element retrieved. Keep the top at the list’s right-hand end. That convention matters because removing from the left side requires moving the remaining elements.

The basic operations

  • Push: add an item with stack.append(value).
  • Pop: remove and return the top item with stack.pop().
  • Peek: inspect the top item with stack[-1] without removing it.
  • Empty check: use if not stack.
  • Size: use len(stack).

Implement a stack with a list

For a stack that only pushes and pops at one end, a list is usually the clearest implementation:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
stack = []

stack.append("first")   # push
stack.append("second")  # push

item = stack.pop()       # returns "second"
print(item)              # second
print(stack)             # ['first']

The rightmost value is the top. After the two pushes, the logical order from bottom to top is first, then second. The call to pop() therefore returns second.

Peeking without removing

if stack:
    top = stack[-1]
    print(f"Top item: {top}")
else:
    print("The stack is empty")

Indexing an empty list with [-1] raises IndexError, so check the stack first unless that exception is the behavior you want.

Processing until empty

stack = ["parse", "validate", "save"]

while stack:
    task = stack.pop()
    print(f"Processing {task}")

This processes save, then validate, then parse. The loop’s truth test is false once the list contains no items.

Time complexity and the correct end to use

The Python complexity reference records list.append as O(1). It records list.pop(k) as O(n-k), so removing the final element with pop() is O(1) in CPython. These figures describe CPython’s built-in list implementation; another Python implementation can have different internal costs. See the official time-complexity table.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Operation Code Typical CPython cost Effect
Push at top append(value) O(1) Adds an item at the right-hand end.
Pop at top pop() O(1) Removes and returns the final element.
Peek at top [-1] O(1) Reads without removing.
Pop at index k pop(k) O(n-k) Elements after k must be shifted.
Pop at bottom pop(0) O(n) All remaining elements may need to move.

Do not implement a stack by inserting at index zero and removing with pop(0). The CPython documentation explains that these operations require O(n) memory movement because the list’s underlying representation must shift its elements; the relevant discussion is in CPython’s collections documentation.

When a deque is a better choice

collections.deque is a double-ended queue. It provides efficient operations at both ends: append, appendleft, pop, and popleft. The standard-library documentation defines and lists these operations in the collections reference.

from collections import deque

stack = deque()
stack.append("first")
stack.append("second")

print(stack.pop())       # second
print(stack[-1])         # first, without removing

For a pure one-ended stack, this has no required correctness advantage over a list. Pick a deque when the same structure may later serve both ends, when callers benefit from an explicit double-ended type, or when your design already uses deque operations elsewhere.

Question List deque
Push and pop on the right Yes; natural syntax Yes
Efficient operations on the left No; pop(0) and insert(0, value) are O(n) Yes; use popleft() and appendleft()
Smallest, simplest stack code Usually Requires an import
Explicit two-ended API Not specialized Yes
Best default for one-end LIFO Usually Use when two-end behavior may matter

Wrap the storage in a custom Stack class

A wrapper is useful when code should not mutate the underlying container directly, or when you need validation, logging, metrics, or a domain-specific exception later. The method names are an API design choice; Python does not require a particular custom class.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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)


work = Stack()
work.push("compile")
work.push("test")
print(work.peek())       # test
print(work.pop())        # test
print(len(work))         # 1

Decide what empty operations mean

The list-backed class above deliberately preserves the container behavior: pop() on an empty stack raises IndexError, and peek() on an empty stack also raises IndexError. That is often preferable because an invalid operation is immediately visible.

Other applications may want a different contract. You could return None, return a caller-supplied default, or raise a domain-specific exception. Make the choice explicit and document it; silently returning None can hide a missing task when None is also a valid value.

A guarded API

class SafeStack:
    def __init__(self):
        self._items = []

    def push(self, value):
        self._items.append(value)

    def pop(self):
        if not self._items:
            raise LookupError("cannot pop from an empty stack")
        return self._items.pop()

    def peek(self):
        if not self._items:
            raise LookupError("cannot peek at an empty stack")
        return self._items[-1]

    def is_empty(self):
        return not self._items

    def __len__(self):
        return len(self._items)

Use a custom exception instead if callers need to distinguish stack exhaustion from other lookup failures. Do not add thread-safety merely because the class has a wrapper; synchronization is a separate design requirement.

Choosing between list, deque, and a wrapper

  1. Use a list when the only operations are push, pop, peek, emptiness, and length at the right-hand end.
  2. Use a deque when you need efficient work at both ends or want that capability represented in the type.
  3. Use a wrapper when you need to restrict mutation, validate values, expose domain language, or control empty-stack errors.

For all choices, keep LIFO behavior visible in tests. A minimal test should push several distinct values, assert that pops return them in reverse insertion order, verify that peeking does not change the length, and check the documented empty behavior.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
def test_stack_order():
    stack = Stack()
    stack.push("a")
    stack.push("b")
    stack.push("c")

    assert stack.peek() == "c"
    assert len(stack) == 3
    assert stack.pop() == "c"
    assert stack.pop() == "b"
    assert stack.pop() == "a"
    assert stack.is_empty()


def test_empty_stack_errors():
    stack = Stack()
    try:
        stack.pop()
    except IndexError:
        pass
    else:
        raise AssertionError("pop() should fail on an empty stack")

Common mistakes and fixes

Removing from the wrong end

Symptom: items come out in insertion order. Cause: code uses pop(0) or removes the first element. Fix: keep the top at the right and call pop().

Peeking with no empty check

Symptom: an unexpected IndexError. Cause: stack[-1] was evaluated after all items were removed. Fix: check if stack, catch the documented exception, or provide a wrapper method with an explicit empty policy.

Mutating a private list

Symptom: callers bypass validation or break invariants. Cause: a wrapper exposes _items or returns it directly. Fix: expose operations such as push, pop, and peek, and return copies or read-only views when inspection is necessary.

Assuming every Python implementation has identical costs

Symptom: a performance assumption fails after changing interpreters. Cause: CPython’s documented list costs were treated as a language-wide guarantee. Fix: qualify complexity claims as CPython behavior and measure the target implementation for performance-critical workloads.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Performance, memory, and reliability notes

Both lists and deques hold references to Python objects; pushing an object does not copy the object itself. A list can grow its internal storage to make repeated right-end appends efficient, but its capacity and allocation details are implementation behavior rather than a stack API guarantee. A deque is organized for double-ended operations and is a better fit when left-end work is part of the workload.

Neither container automatically limits size. If input can grow without bound, enforce a maximum length or reject new values before memory usage becomes a reliability problem. A bounded deque can discard or reject entries according to the behavior you choose, but that policy must be explicit because discarded stack entries change the semantics.

Stacks are not queues: a queue removes the oldest item, whereas a stack removes the newest. If you need producer-consumer FIFO behavior, choose a queue-oriented design rather than reversing a stack’s meaning.

Or skip the browser setup

If you are creating documentation or visual regression images for a stack demo, ScreenshotNeo can capture the rendered page through one request instead of requiring you to configure a browser. Before capture it accepts cookie or consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the page verdict and billing status in X-Page-Verdict and X-Billed headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The API supports full-page captures with lazy images loaded, CSS-selector element captures, dark mode, device presets or custom viewports, retina scale, PDFs, custom CSS and JavaScript, clicks before capture, selector or network-idle waits, request blocking, custom headers and cookies, authorization, timezone and geolocation, transparent backgrounds, resizing, chosen cache TTLs, signed image links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, a usage API, and an OpenAPI specification.

See the ScreenshotNeo API documentation for request options. This example captures a page as WebP:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

The same request in Python:

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)

And in Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

The Free plan includes 1,000 screenshots each month with no card. Paid plans start at $5 for 3,000 shots; every feature is available on every plan, and yearly billing provides two months free. Create a free ScreenshotNeo account to start without a card.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.