Learning Goals
3 minBy the end of this lesson you can:
- Build a Stack class with push, pop, peek, size, is_empty.
- Recognise three classic stack problems: balanced brackets, undo/redo, function calls.
- Know that a Python list IS a stack —
appendandpopare both O(1). - Spot "LIFO" in real systems and pick a stack as the data structure.
Warm-Up · The Plate Stack
5 minImagine a stack of plates. You can only do three things: add a plate on top, take the top plate off, or look at the top plate. You can't reach into the middle.
push A → [A]
push B → [A, B]
push C → [A, B, C] ← top is C
peek → C (look, don't remove)
pop → C (remove and return)
[A, B] now
pop → BLast in (C), first out. LIFO.
A stack restricts access to one end. That restriction is the point — it makes operations O(1), and it's the right shape for any "most-recent thing first" problem.
New Concept · The Stack API
14 minA Python list is already a stack
stack = [] stack.append("A") # push stack.append("B") stack.append("C") print(stack[-1]) # peek — top is C print(stack.pop()) # pop — returns C, stack is now ["A", "B"] print(len(stack)) # size print(stack == []) # is_empty?
List append is O(1) amortised; list pop() with no index is O(1). Python lists are essentially dynamic arrays, and the end is fast.
A wrapper class · for the named interface
Often you want a class that only allows stack operations — restricting the API to push/pop/peek is the point:
class Stack: def __init__(self): self._items = [] def push(self, item): self._items.append(item) def pop(self): if not self._items: raise IndexError("pop from empty stack") return self._items.pop() def peek(self): if not self._items: raise IndexError("peek into empty stack") return self._items[-1] def __len__(self): return len(self._items) def is_empty(self): return len(self._items) == 0 def __repr__(self): return f"Stack(top → {self._items[::-1]})"
Encapsulated. Outside code can't poke into the middle. _items is private; the four methods are the contract.
Classic application 1 · Balanced brackets
def balanced(s): pairs = {")": "(", "]": "[", "}": "{"} opens = set("([{") stack = [] for ch in s: if ch in opens: stack.append(ch) elif ch in pairs: if not stack or stack.pop() != pairs[ch]: return False return not stack # all closed? print(balanced("(a + [b * c] - {d})")) # True print(balanced("(a + [b)")) # False — wrong type print(balanced("(a + b")) # False — unclosed print(balanced("a)")) # False — extra close
Every open bracket goes on the stack. Every close pops the top and checks. If the wrong type pops, or there's no opener, fail. If the stack isn't empty at the end, unmatched opens.
Classic application 2 · Undo/redo
class Editor: def __init__(self): self.text = "" self._undo_stack = [] self._redo_stack = [] def type(self, s): self._undo_stack.append(self.text) self.text += s self._redo_stack.clear() # any new edit invalidates redo def undo(self): if not self._undo_stack: return self._redo_stack.append(self.text) self.text = self._undo_stack.pop() def redo(self): if not self._redo_stack: return self._undo_stack.append(self.text) self.text = self._redo_stack.pop() e = Editor() e.type("Hello "); e.type("world"); e.type("!") print(e.text) # Hello world! e.undo() print(e.text) # Hello world e.undo() print(e.text) # Hello e.redo() print(e.text) # Hello world
Two stacks. Undo pops from undo, pushes to redo. Redo pops from redo, pushes to undo. Every text editor works this way.
Classic application 3 · The call stack itself
When a function calls another, Python pushes a stack frame. When it returns, Python pops. RecursionError happens when you exceed the maximum stack depth. Recursion is "using the call stack as data" — every recursive function builds and unwinds a stack.
Worked Example · RPN Calculator
12 minReverse Polish Notation is "operator after the operands". 3 4 + means 3 + 4. RPN is a poster child for stacks. Save as rpn.py:
# rpn.py — evaluate Reverse Polish Notation class Stack: def __init__(self): self._items = [] def push(self, x): self._items.append(x) def pop(self): return self._items.pop() def __len__(self): return len(self._items) OPS = { "+": lambda a, b: a + b, "-": lambda a, b: a - b, "*": lambda a, b: a * b, "/": lambda a, b: a / b, } def evaluate(expr): stack = Stack() for token in expr.split(): if token in OPS: b = stack.pop() a = stack.pop() stack.push(OPS[token](a, b)) else: stack.push(float(token)) return stack.pop() # Examples print(evaluate("3 4 +")) # 7.0 print(evaluate("5 1 2 + 4 * + 3 -")) # 14.0 # (1 + 2) * 4 + 5 - 3 # = 3 * 4 + 5 - 3 # = 12 + 5 - 3 = 14
Read the diff
Number tokens get pushed. Operator tokens pop two values, apply, push the result. At the end, one value remains — the answer. No parser, no parentheses logic. RPN's genius is that it doesn't need them — the stack tracks the structure.
HP scientific calculators in the 70s-80s used RPN. Engineers loved them — no brackets, less typing for complex expressions. The stack is so ergonomic it became a cult interface.
Try It Yourself
13 minBuild the full Stack class from the concept section. Test by pushing 5 items, peeking, popping all, confirming is_empty.
Use a stack to reverse "Aisyah" without using [::-1] or any slicing.
Hint
def reverse_with_stack(s): stack = [] for ch in s: stack.append(ch) out = "" while stack: out += stack.pop() return out print(reverse_with_stack("Aisyah")) # 'hayisA'
Push everything; pop everything. The pop order is the reverse of the push order. Classic stack trick.
Modify the balanced function to also handle angle brackets <> and to ignore characters inside string literals (text inside double quotes).
Hint
def balanced(s): pairs = {")": "(", "]": "[", "}": "{", ">": "<"} opens = set("([{<") stack = [] in_string = False for ch in s: if ch == '"': in_string = not in_string continue if in_string: continue if ch in opens: stack.append(ch) elif ch in pairs: if not stack or stack.pop() != pairs[ch]: return False return not stack print(balanced('<a href="(test)">link</a>')) # True (parens inside string don't count) print(balanced('<a href="(test></a>')) # False (string never closes)
String-aware bracket matching is a tiny step toward writing a parser. Real parsers handle escapes, comments, etc. — same idea, more bookkeeping.
Mini-Challenge · Browser History
8 minBuild a Browser class that supports visit(url), back(), forward() and current(). Visiting a new page after going back should clear the forward history.
b = Browser() b.visit("page1") b.visit("page2") b.visit("page3") print(b.current()) # page3 b.back() print(b.current()) # page2 b.back() print(b.current()) # page1 b.forward() print(b.current()) # page2 b.visit("page4") # this clears the forward history b.forward() # nothing to go forward to print(b.current()) # page4
Show one possible solution
class Browser: def __init__(self): self._back = [] # stack of pages visited before current self._current = None self._forward = [] # stack of pages we can go forward to def visit(self, url): if self._current is not None: self._back.append(self._current) self._current = url self._forward.clear() # new visit invalidates forward history def back(self): if not self._back: return self._forward.append(self._current) self._current = self._back.pop() def forward(self): if not self._forward: return self._back.append(self._current) self._current = self._forward.pop() def current(self): return self._current b = Browser() b.visit("page1"); b.visit("page2"); b.visit("page3") b.back(); b.back() b.forward() b.visit("page4") # clears forward print(b.current()) # page4
Non-negotiables: two stacks (back and forward) plus the current page. visit clears forward, back/forward trade between the stacks. Two stacks is enough for the entire browser history feature.
Recap
3 minA stack is a collection with three operations — push, pop, peek — restricted to one end. Last in, first out. Python lists do this for free (append and pop). A wrapper class is useful when you want to forbid middle access. Stacks are everywhere: brackets, undo/redo, browser history, function frames, RPN. Whenever you see "most recent first", reach for a stack.
Vocabulary Card
- stack
- A LIFO collection. Push, pop, peek.
- LIFO
- Last in, first out.
- push / pop / peek
- Add, remove, look. All O(1).
- call stack
- The interpreter's stack of pending function calls. Recursion lives here.
Homework
4 minBuild infix_to_postfix.py. Convert an infix expression (e.g. "3 + 4 * 2") to postfix ("3 4 2 * +") using the classic Shunting-yard algorithm. Numbers go straight to output; operators go on a stack and pop based on precedence.
Sample · Shunting-yard
PRECEDENCE = {"+": 1, "-": 1, "*": 2, "/": 2} def infix_to_postfix(expr): output = [] op_stack = [] for token in expr.split(): if token in PRECEDENCE: while (op_stack and op_stack[-1] in PRECEDENCE and PRECEDENCE[op_stack[-1]] >= PRECEDENCE[token]): output.append(op_stack.pop()) op_stack.append(token) else: output.append(token) while op_stack: output.append(op_stack.pop()) return " ".join(output) print(infix_to_postfix("3 + 4 * 2")) # 3 4 2 * + print(infix_to_postfix("3 + 4 - 1")) # 3 4 + 1 - print(infix_to_postfix("3 * 4 + 2")) # 3 4 * 2 +
Non-negotiables: operator stack, precedence table, output list. Numbers go straight to output; operators wait on the stack until a higher-precedence one shows up or the expression ends. Pair with the RPN evaluator from the worked example for a complete infix calculator.