python/volume-4/chapter-51ch 51 / 64 · 9 min

51The stack — last in, first out

In Chapter 50 we strung data together with pointers. Each node reached across the heap to the next, so the list could grow wherever memory had room. Here we do almost the opposite. We take one plain list, hold it to a single promise, and out falls the most useful container in all of computing. A stack is the simplest deal you can strike with memory: you agree to add and remove items at one end only, the top, so the last thing in is the first thing out. Here's the plan. We'll watch that one rule, Last In, First Out, make push, pop, and peek all cost O(1). Then we'll find the exact same shape hiding under function calls, undo, the browser Back button, and matching brackets. The whole way through we keep asking the one question that matters: when is the next thing I need always the thing I added most recently? By the end you'll see the stack that runs underneath every program you've ever written, write a bracket-matcher from scratch, and recognise a LIFO access pattern on sight.

One end, and the world's simplest rule Why push, pop, and peek are all O(1) A stack is not a type — it's a discipline The stack you're already standing on
scroll to begin

01One end, and the world's simplest rule

Let's start with something you've physically used a hundred times: a spring-loaded stack of cafeteria trays. You can do exactly two things with it. Drop a tray on top, or lift the top tray off. You can't slide one out of the middle, and you can't reach the bottom tray until every tray above it is gone. Watch what that one constraint forces: whatever went on last comes off first. That's the whole idea, and it has a name — LIFO, Last In, First Out. The tray you can currently touch is the top. Adding is push. Removing the top is pop. Looking at the top without removing it is peek.

Now the memory picture, and here's the surprise: a stack is not some exotic new object. You already own it. A Python list, as we saw back in Volume 1, is a contiguous block of 8-byte references with some spare slots on the end (Vol 1, ch 6). A stack is just that list with a promise: I will only ever touch the far end. Push is append. Pop is .pop(). Peek is a[-1]. There's no separate "top pointer" stored anywhere. CPython keeps the list's length in its header, so the top is simply the last index, len − 1. Let's look at the block sitting in RAM before we touch a single line of code.

a stack IS a list (Vol 1) — a contiguous block of 8-byte references, touched at one end 01234567 'do' 're' 'mi' 'fa' 'sol' free free free spare cells — overallocated (Vol 1, ch 6) base+8+16+24+32+40 top = a[len−1] the ONLY end you touch push → drop a ref here, len+1 pop → clear top, len−1 str 'mi' (heap) each slot = one 8-byte reference → an object on the heap (~28 B header)
Fig — A stack lives in one contiguous run of reference slots. Everything happens at the high end: push writes into the next free cell and bumps the length; pop clears the top cell and drops the length by one. The other slots — and the whole bottom of the stack — are never read or moved.

What does one item cost? I measured it on CPython 3.12. An empty list is 56 bytes — a bare header, no data block yet. A list of a million integers is 8,000,056 bytes, almost exactly 8 bytes per element. That's because the block stores only the reference; the integer object it points at lives elsewhere on the heap. So a stack is about as light as a container gets: 8 bytes of "plumbing" per item, plus a little overallocated slack on the end.

If only the top ever moves, how expensive can any operation possibly be? Let's derive the cost from the layout instead of taking it on faith. →

02Why push, pop, and peek are all O(1)

Reason straight from the block. To push, CPython looks at the length, finds the next free slot right after the last item, writes one reference into it, and increments the length. It does not look at slot 0, or slot 3, or the million items underneath. It touches exactly one cell. Work that doesn't depend on how big the stack is means O(1). To pop, it reads the top cell, hands the reference back, and decrements the length — again one cell, O(1). To peek, it reads a[-1] and returns it without changing anything — O(1). Three operations, none of which ever walks the block.

There's one honest wrinkle. Every so often a push finds no free slot left, so CPython allocates a bigger block and copies the references over. That single push is O(n). But it happens rarely, and the growth is geometric, so averaged over many pushes the cost comes out to a constant. This is amortized O(1) (Vol 3, ch 3), the same accounting that makes list.append cheap. I watched the block grow in jumps. Pushing onto a fresh list, getsizeof stepped 56 → 88 → 120 → 184 → 248 → 312 → 376…, reallocating at sizes 1, 5, 9, 17, 25, 33 — a handful of copies across dozens of pushes, each one buying room for the next batch.

cost.pypython
stack = []
stack.append('do')      # push   O(1)  -> ['do']
stack.append('re')      # push   O(1)  -> ['do', 're']
stack.append('mi')      # push   O(1)  -> ['do', 're', 'mi']
top = stack[-1]         # peek   O(1)  -> 'mi'   (stack unchanged)
gone = stack.pop()      # pop    O(1)  -> 'mi'   (stack is ['do', 're'])

Line by line: three appends push onto the far end, each dropping a reference into the next slot. stack[-1] peeks — it reads the top without disturbing it, so the list still has three items. stack.pop() removes and returns the top, 'mi'. The last thing pushed is the first thing out, LIFO made literal. Timed at a million operations each, a push averaged about 26 nanoseconds and a pop about 24 nanoseconds on my machine. Wall-clock time varies by hardware; the point is that neither grows with the stack's size.

before push('fa') do re mi top after — one cell written, top +1 do re mi fa top every slot beneath the top — and the whole bottom — is never read or moved
Fig — One push is a single reference write into the next free cell plus a length bump. Because the block is contiguous, a run of pushes streams through the same cache lines — O(1) and cache-friendly.
Push and pop the RIGHT end
The O(1) is a gift of touching the high end. Touch the low end and it evaporates: stack.pop(0) removes item 0 and then shifts every remaining reference down one slot to close the gap — O(n) per call. Draining 50,000 items with pop(0) took me ~1.9 seconds; draining the same 50,000 with pop() took ~0.0014 seconds — roughly 1,370× slower for using the wrong end. A stack always pops from the top, so it never pays this tax — but only if you keep the discipline.
InteractivePush and pop — watch the top move, and only the top
empty stack — nothing pushed yet
len 0
Push adds at the top; pop removes the top. Push A, B, C then pop three times and they come back C, B, A — the reversal is the LIFO signature.

So a stack is a list you agreed to touch at one end. But hold on — Python never made you declare a "Stack". Where is the stack, exactly? →

03A stack is not a type — it's a discipline

Here is the reframe that trips almost everyone up. In many languages a stack is a class you import and instantiate. In Python, a stack is not a data type at all — it's a rule you impose on a list. The moment you promise yourself "I will only ever append and pop(), never index into the middle, never pop(0)," that ordinary list is a stack. The structure isn't in the object. It's in the discipline. Nothing in the runtime enforces it. The LIFO-ness is a contract you keep with yourself, and its whole payoff is that every operation stays O(1) and the code reads as "a stack."

↺ The thing people get backwards
People hunt for a Stack class and feel something is missing when Python doesn't ship one. Backwards. Python does ship the stack — it's list, restricted. The insight worth stealing is that a data structure is often not a special object but an access pattern laid over a general one. "Stack" names how you use the list, not what it is. Get this and half of "data structures" stops being about exotic types and starts being about choosing which end to touch. Name the discipline in your code — call it stack, use only append/pop — and you've documented intent and sidestepped the O(n) pop(0) trap in one move.

Myth

"To use a stack in Python I need a special Stack class or a library."

Reality

A plain list is a stack: append = push, pop() = pop, a[-1] = peek — all O(1). The "stack" is the promise to touch only the end, not a type you import.

You do have choices for the backing, and they matter for reasons Volume 3 made physical. A list keeps its references contiguous, so a run of pushes and pops streams through the same cache lines — the CPU prefetcher loves it. The textbook alternative is a linked stack: one small node per item, each holding a value and a next pointer. Push makes a node and points it at the old top; pop follows next. That's correct and also O(1), but every node is a separate heap allocation scattered across RAM. So pushing and popping means allocation plus pointer-chasing, and each hop risks a cache miss (Vol 3, ch 2). I measured the gap. Pushing and popping 2,000,000 items was about 0.20 s on a list versus 0.88 s on a node-per-item stack — same Big-O, roughly 4× slower on the metal. The node is heavier too: a minimal two-field node with __slots__ is 48 bytes each, ballooning to ~344 bytes if it carries an ordinary __dict__, against the list's flat 8 bytes per item.

list-backed stack — contiguous 8 B/item · one block · streams through cache top neighbours in the same cache line → linked stack — one node per item 48 B/node · scattered · a cache miss per hop top next · next · next — pointer-chasing across RAM Same O(1) per op — but contiguous beats scattered on real hardware (~4× here).
Fig — Two correct stacks, very different metal. The list keeps items shoulder-to-shoulder so the CPU streams and prefetches; the linked stack scatters a 48-byte node per item and chases pointers. Prefer the list unless you specifically need a persistent or shared structure.
When you'd pick something other than a list
Two real cases. A collections.deque is also O(1) at the end and never needs to reallocate the whole block, so it's a fine stack when you're churning huge volumes. And queue.LifoQueue is a stack wrapped in a lock — use it only when multiple threads share one stack, since the locking makes it markedly slower for single-threaded code. For everything else, a bare list is the right, fast default.

You've been imposing this discipline by hand. Now meet the stack you never declared — the one the interpreter has been running under your feet this whole time. →

04The stack you're already standing on

Every time one function calls another, the machine has to remember where to come back to and what the caller's local variables were. It stores that in a frame, a little record holding a call's local names and its return address (Vol 1, ch 9). And it keeps those frames in exactly one arrangement: a stack. When a calls b calls c, three frames are pushed. When c returns, its frame is popped and control resumes in b. It must be LIFO, because the function that started most recently is the one that has to finish first, before the one that called it can continue. This is the call stack, and it is the most important stack you will ever use. You use it in every program without ever typing push.

a() calls b() calls c() calls show_frames() — frames stack up <module> (bottom) a() b() c() show_frames() ← running top of the call stack a call pushes a return pops
Fig — The call stack is a real stack of frames. Each call pushes a frame on top; each return pops the top frame and hands control back to the one beneath. The frame on top is the code running right now.

You can see it directly. CPython links each frame to its caller through f_back. So walking that chain from the running function outward is reading the stack top-to-bottom.

callstack.pypython
import sys

def show_frames():
    f = sys._getframe()             # the frame running right now (top of the stack)
    names = []
    while f:                        # walk f_back all the way down
        names.append(f.f_code.co_name)
        f = f.f_back                # step to the caller's frame
    print(" -> ".join(names))

def a(): b()
def b(): c()
def c(): show_frames()
a()                                 # show_frames -> c -> b -> a -> <module>

Reading it: sys._getframe() hands back the frame on top, the one for show_frames itself. The while loop follows f_back link by link, and each link is the frame that called this one, so we climb down the stack recording names. The real output was show_frames -> c -> b -> a -> <module>. That's the exact push order read back in reverse, which is what "popping" would give you.

This is also why recursion costs memory and why it can crash. Each recursive call pushes another frame, and the frames only start popping when a base case lets calls return. Recur without a base case and you push frames forever, until the stack runs out of room. Python guards against blowing the real, finite C stack underneath by capping its own depth. sys.getrecursionlimit() is 1000 by default, and an unbounded recursion raised RecursionError at a depth of about 999 when I ran it. That crash has a famous name.

InteractiveThe wall recursion hits — and the stack that walks right through it
how deep can a LIFO go? — log-scaled depth, same push/pop either way CALL STACK · recursion — every call pushes a frame ✓ survives recursion limit · 1,000 EXPLICIT STACK · a list on the heap — every push is one reference ✓ survives ≈134 M · ~1 GB 1 1,000 1 M 1 B Both stacks fine at depth 100. Recursion still has headroom — nothing has overflowed.
depth 100
Drag past 1,000. Recursion dies with a stack overflow — yet the heap bar has barely moved: the call stack lives in a few megabytes, a list lives in the heap's gigabytes. Same LIFO, ~134,000× more room. This is exactly why a deep DFS is rewritten from recursion into an explicit stack.
Where you meet this — "stack overflow"
The error you hit when recursion runs away is a stack overflow — the call stack outgrew its space. The Q&A site every programmer lives on is named after it. Deep down, a "stack overflow" attack in security works the same way: overrun a function's frame on the call stack to overwrite the return address. Every debugger's stack trace (or "traceback") is literally that frame stack printed top-to-bottom. And when you convert a recursive DFS into an explicit stack to avoid the limit (Vol 3, ch 13), you're just moving the same LIFO structure from the interpreter's stack onto one of your own.
The deeper cut
Two honest refinements. First, in CPython the frame objects are heap-allocated PyFrameObjects, and the interpreter keeps them in a last-in-first-out chain via f_back — a stack, though not one contiguous slab the way the trays diagram suggests. Underneath that sits the actual C call stack (the hardware one, growing downward in memory), and the recursion limit exists to stop Python-level recursion from overflowing it and hard-crashing the process. Second, the limit is adjustable — sys.setrecursionlimit(n) — but raising it too far just trades a catchable RecursionError for an uncatchable segfault, because the C stack is the real ceiling. This is exactly why deeply recursive algorithms are often rewritten with an explicit stack: your own list can grow into the heap's gigabytes, while the call stack is capped at megabytes.

The call stack matches nested function calls for a living — open a, open b, close b, close a. That's the same shape as nested brackets. Let's make a stack check them by hand. →

05Matching brackets — a stack's party trick

Is ([{}]) balanced? Is ([)]? A human eyes it; a stack proves it in one pass. The rule that makes brackets legal is simple: the one you opened most recently must be the one you close first. That's LIFO exactly. So scan left to right. Every time you meet an opener, push it. Every time you meet a closer, the thing it must match is whatever is on top of the stack, so pop and check they're a pair. If they don't match, or the stack is empty when you need something to pop, it's broken. If you reach the end with an empty stack, every opener found its partner — balanced.

brackets.pypython
def balanced(s):
    pairs = {')': '(', ']': '[', '}': '{'}
    stack = []
    for ch in s:
        if ch in '([{':                       # an opener → push it
            stack.append(ch)
        elif ch in ')]}':                      # a closer → the top must be its partner
            if not stack or stack.pop() != pairs[ch]:
                return False                   # nothing to match, or a mismatch
    return not stack                           # balanced only if nothing is left open

for t in ['()', '([])', '([)]', '(((', '{[()]}', '](']:
    print(repr(t), '->', balanced(t))

Walk it. pairs maps each closer to the opener it needs. For an opener we appendpush. For a closer we check two failure modes at once: not stack (a closer with nothing open) and stack.pop() != pairs[ch] (the most recent opener is the wrong kind). Reach the end, and not stack asks "is everything closed?" I ran it, and it returns exactly what your eye does: '()' -> True, '([])' -> True, '([)]' -> False (the ) tries to close a [), '(((' -> False (three left open), '{[()]}' -> True, and ']( ' -> False. Twelve lines, and it's the beating heart of every compiler, JSON parser, and the rainbow-bracket highlighter in your editor.

scan '([{}])' left → right: push on an opener, pop-and-check on a closer ( [ { } ] ) push push push pop pop pop stack at the deepest point ( [ { top = the opener the next closer must match ends empty → ✓ balanced
Fig — Openers pile up on the stack; each closer pops the top and checks it's the right partner. The top always holds the innermost open bracket — the one that must close next — which is exactly why LIFO is the natural fit. Finish with an empty stack and every bracket was matched.
A matcher, not yet a parser
This twelve-line version treats every character as code, so it would wrongly reject a string literal like ")" or a comment that contains a lone bracket. A real language parser runs the same stack but first splits the text into tokens, so brackets inside strings and comments don't count. The stack is the right engine; feeding it the right symbols is the other half of the job.
InteractiveType brackets — watch the stack push, pop, and catch a mismatch
stack (openers waiting to be closed): scan through the string →
0 / 6
Openers push; closers pop and must match the top. Try ([)] — it's the classic trap the stack catches the instant a closer meets the wrong opener.

Push to remember, pop to go back. That "go back" is not a toy — it's the mechanism behind undo, the browser Back button, and depth-first search. →

06The Back button of everything

Once you can see LIFO, you start finding it everywhere, because a huge class of features is really one wish: "take me back to the thing I was just at." That is a pop. Undo is a stack of past states. Every action you take pushes a way to reverse it; Ctrl-Z pops the most recent and applies it. Add a second stack for the ones you undo and you get redo: undo pops from one stack and pushes onto the other. Your browser's Back button is a stack of pages. Each link you follow pushes the current page; Back pops it and returns you. Depth-first search (Vol 3, ch 13) explores a maze by pushing each junction and popping back to the last one when it hits a dead end. Recursion does this on the call stack, or you carry an explicit stack to do it yourself. Even a calculator evaluating 3 + 4 × 2 leans on a stack to hold operands. You press Back without a thought; you were popping a stack.

undo / redo = two stacks, passing the top item back and forth UNDO stack type "H" type "i" bold on ← top REDO stack (empty) Ctrl-Z: pop undo → push redo Ctrl-Y: pop redo → push undo
Fig — Undo and redo are two stacks trading their tops. Ctrl-Z pops your last action off the undo stack, reverses it, and pushes it onto the redo stack; Ctrl-Y does the mirror. LIFO is why undo always reverses your most recent action first.
The one-line tell for reaching for a stack
Being "good at data structures" is mostly matching an access pattern to a container, and the stack's pattern is one of the easiest to name. Ask: is the next thing I need always the thing I added most recently? Undo the last edit, return to the last page, back out of the last dead-end, close the innermost bracket, finish the deepest call — all yes. Whenever the job is "handle things in the reverse of the order they arrived," or "remember where I was so I can come back," reach for a stack. If instead you need the oldest waiting item first — fairness, a queue at a counter — that's the opposite discipline, and it's the next chapter.
The deeper cut
A stack is also the cleanest way to reverse a sequence: push every item, then pop them all, and they come out backwards — the reason LIFO and "reverse order" are two names for the same effect. This is why converting infix arithmetic to postfix (Reverse Polish Notation) and then evaluating it both use stacks: one stack holds operators while you flip the order, another holds operands while you compute. It's also why a depth-first traversal visits nodes in the opposite order to the one you'd get by pushing them onto a queue — swap the stack for a queue and depth-first search becomes breadth-first, changing nothing but the container. Same nodes, same edges; the discipline of the container decides the shape of the search.
Wait —
if a stack always serves the newest item, what serves the oldest — the one that's been waiting longest, the fair one?

That's the stack's mirror image: the queue, First In, First Out — the structure of every line you've ever stood in, every print job, and the OS scheduler deciding who runs next. It's cheap at both ends for a reason we'll build from memory up. →

PROGRAMS run them line by line — the same ideas, written the many ways Python offers
Chapter 51, in working code

Twelve tiny programs, and not one of them imports a Stack class. Python already ships the stack as a plain list you hold to a single promise. Out of that one promise fall reversal, recursion, bracket-matching, undo, and the Back button of everything.

One list, one promise
A stack is not a type you import — it's a list plus a vow to touch only the far end. Push is append, pop is .pop(), peek is a[-1]. The last thing in is the first thing out, and that reversal is the whole idea.
Why the top is always O(1)
Reason straight from the memory block. Push and pop touch exactly one cell at the high end, no matter how big the stack is — that's O(1). Touch the low end instead and the cost explodes. The linked alternative is correct too, but pays in pointer-chasing.
The stack you're already standing on
Every function call pushes a frame; every return pops one — the call stack is a real stack you use without typing push. That's why recursion is a stack, why deep recursion overflows, and why you can always trade the call stack for an explicit list of your own.
Where the stack hides
Once you can see LIFO you find it everywhere: the next thing you need is the thing you added most recently. Matching brackets, undo and redo, and a calculator evaluating an expression are all the same stack, wearing different clothes.
end of chapter 51 · six sections
iolinked.com
Written by Ajai Raj