PYTH-201 · Lesson 3 of 12 · 25 min
Stacks and queues
▶ Video coming soon A video version of this lesson is on the way.
Many problems only ever touch one end of a collection. The cafe's order screen needs an Undo button that reverses the most recent change first. The online shop packs orders in the order they arrived, so the oldest waiting order is always next. The roaster's control panel shows the average of the last ten temperature readings, forgetting older ones as new ones arrive. In each case, items enter and leave in a strict pattern, and choosing a structure built for that pattern makes the code simpler and faster.
This lesson introduces two of the most widely used structures in computing. A stack hands back the most recent item first; a queue hands back the oldest. You will build both, see why a Python list is perfect for a stack but poor for a queue, use collections.deque for queues and sliding windows, and apply them to real tasks: undo, checking brackets, packing orders and simulating the cafe line.
Abstract data types
A stack and a queue are abstract data types (ADTs): they are defined by the operations they allow and the rules those operations follow, not by how they are stored. A stack promises push, pop and peek with last-in, first-out behavior; how the items are kept is a separate decision called the implementation. Separating the two lets you choose or change the implementation later without touching the code that uses the structure, the same idea as the classes and interfaces of PYTH-102.
For each ADT in this course, ask two questions: what operations does the problem need, and which implementation makes those operations cheap? For stacks and queues, the answer to the second question is that every core operation should be O(1).
Stacks: last in, first out
A stack works like a pile of cups at the cafe counter: you add a cup to the top and take the top cup off. The last item added is the first removed, which is called LIFO (last in, first out). The standard operations are push (add to the top), pop (remove and return the top), peek (look at the top without removing it) and a test for emptiness.
A Python list is an excellent stack if you treat its end as the top. append is push and pop() is pop, both O(1), and items[-1] is peek. Wrapping the list in a small class makes the intent clear and prevents code from reaching into the middle.
class Stack:
"""A last-in, first-out stack built on a list (the end of the list is the top)."""
def __init__(self):
self._items = []
def push(self, item):
self._items.append(item)
def pop(self):
if not self._items:
raise IndexError("pop from an empty stack")
return self._items.pop()
def peek(self):
if not self._items:
raise IndexError("peek at an empty stack")
return self._items[-1]
def __len__(self):
return len(self._items)
cups = Stack()
for size in ["small", "medium", "large"]:
cups.push(size)
print(cups.peek(), len(cups))
print(cups.pop(), cups.pop(), len(cups))large 3 large medium 1
Undo with two stacks
Undo is the classic stack application. Every change made on Mei's order screen is pushed onto an undo stack. Pressing Undo pops the most recent change and reverses it, so the changes come back off in exactly the opposite order to the one in which they were made. Adding a redo feature needs a second stack: undone changes are pushed onto the redo stack, and any brand-new change clears it, because redoing an old change after a new one would make no sense.
class Order:
def __init__(self):
self.lines = {}
self._undo, self._redo = [], []
def set_qty(self, item, qty, record=True):
old = self.lines.get(item, 0)
if qty:
self.lines[item] = qty
else:
self.lines.pop(item, None)
if record:
self._undo.append((item, old))
self._redo.clear() # a new change makes redo meaningless
def undo(self):
item, old = self._undo.pop()
self._redo.append((item, self.lines.get(item, 0)))
self.set_qty(item, old, record=False)
def redo(self):
item, qty = self._redo.pop()
self._undo.append((item, self.lines.get(item, 0)))
self.set_qty(item, qty, record=False)
o = Order()
o.set_qty("latte", 2)
o.set_qty("muffin", 1)
o.set_qty("latte", 3)
print(o.lines)
o.undo(); print(o.lines)
o.undo(); print(o.lines)
o.redo(); print(o.lines){'latte': 3, 'muffin': 1}
{'latte': 2, 'muffin': 1}
{'latte': 2}
{'latte': 2, 'muffin': 1}Each entry on the undo stack records what is needed to reverse one change, here the item and its previous quantity. Real editors use the same design, often storing command objects that know how to undo themselves. Stacks also appear inside Python itself: every function call pushes a frame onto the call stack and every return pops it, which lesson 6 explores with recursion.
Matching brackets
A stack also solves matching problems, where each opening item must be closed in reverse order. Sam Okoye is writing a checker for the shop's price-rule formulas, such as (base * (1 + tax)) - [discount], and wants to reject formulas with unbalanced brackets before they reach the shop. Scan the text left to right: push each opening bracket, and for each closing bracket pop the top of the stack and check that it is the matching opener. The text is balanced if no mismatch occurs and the stack is empty at the end.
PAIRS = {")": "(", "]": "[", "}": "{"}
def balanced(text):
stack = []
for ch in text:
if ch in "([{":
stack.append(ch)
elif ch in PAIRS:
if not stack or stack.pop() != PAIRS[ch]:
return False
return not stack
for formula in ["(base * (1 + tax)) - [discount]", "(base * (1 + tax)", "[price)", "a + b)"]:
print(f"{formula:<34} {balanced(formula)}")(base * (1 + tax)) - [discount] True (base * (1 + tax) False [price) False a + b) False
Try it yourself
Balanced brackets
Write is_balanced(text) that returns True if every (, [ and { in the text is closed by the matching bracket in the right order, and False otherwise. Other characters are ignored.
Use a list as a stack. The starter only counts brackets, so it wrongly accepts "([)]".
def is_balanced(text):
opens = sum(text.count(c) for c in '([{')
closes = sum(text.count(c) for c in ')]}')
return opens == closes
print(is_balanced('([)]'))Queues: first in, first out
A queue works like the line at the cafe counter: people join at the back and are served from the front, so the first to arrive is the first served. This is FIFO (first in, first out). The operations are enqueue (add at the back), dequeue (remove from the front) and peek at the front.
A list makes a poor queue. Appending at the back is fine, but removing from the front with pop(0) shifts every remaining item, O(n), as lesson 2 showed. The standard library's collections.deque (pronounced deck, short for double-ended queue) is designed for this job: append, appendleft, pop and popleft are all O(1). Internally it is a chain of fixed-size blocks, so neither end ever needs to shift the items in the middle.
from collections import deque
packing = deque()
for order_id in [1041, 1042, 1043]:
packing.append(order_id) # enqueue at the back
print("next to pack:", packing[0])
print("packed", packing.popleft()) # dequeue from the front
packing.append(1044)
print("waiting:", list(packing))
print("packed", packing.popleft(), "then", packing.popleft())
print("still waiting:", len(packing))next to pack: 1041 packed 1041 waiting: [1042, 1043, 1044] packed 1042 then 1043 still waiting: 1
deque supports most list operations, including len, iteration, in and indexing near the ends, but indexing in the middle of a long deque is O(n), so use it for its ends. The table compares the two choices for each core operation.
| Operation | list | deque |
|---|---|---|
| Add at the back | append: O(1) amortized | append: O(1) |
| Remove from the back | pop(): O(1) | pop(): O(1) |
| Add at the front | insert(0, x): O(n) | appendleft: O(1) |
| Remove from the front | pop(0): O(n) | popleft(): O(1) |
| Index in the middle | O(1) | O(n) |
Some systems implement a queue with a fixed-size list used as a ring, called a circular buffer: two indexes mark the front and the back, and each wraps around to the start of the list when it reaches the end. Nothing ever shifts, so both ends are O(1), and the memory never grows. Network cards, audio players and the roaster's sensor firmware use ring buffers because their size is known in advance. In Python you rarely need to write one, since deque already provides O(1) ends, but recognizing the idea helps when you read code in other languages or work with hardware. Whatever the implementation, the contract is the same: items leave in the order they arrived.
Try it yourself
A FIFO packing queue
Write a class PackingQueue backed by a collections.deque. It needs add(order_id) to join the back of the queue, next() to remove and return the order at the front, and __len__.
If next() is called on an empty queue, return None instead of raising an error. The starter uses a list with pop(), which serves the newest order first.
class PackingQueue:
def __init__(self):
self._orders = []
def add(self, order_id):
self._orders.append(order_id)
def next(self):
return self._orders.pop()
def __len__(self):
return len(self._orders)
q = PackingQueue()
q.add(1041)
q.add(1042)
print(q.next())Sliding windows with a bounded deque
A deque created with maxlen keeps only the most recent items: once full, adding at one end silently drops an item from the other. That is exactly a sliding window. Kai's roaster panel shows the average of the last few drum temperature readings, which smooths out noise from the sensor; a deque with maxlen holds the window, and a running sum avoids re-adding the whole window on every reading.
from collections import deque
def smoothed(readings, size=3):
window, total, out = deque(maxlen=size), 0.0, []
for r in readings:
if len(window) == size:
total -= window[0] # the oldest reading is about to fall off
window.append(r)
total += r
out.append(round(total / len(window), 1))
return out
temps = [196.0, 201.5, 199.0, 205.5, 210.0, 208.5, 212.0]
print(smoothed(temps))[196.0, 198.8, 198.8, 202.0, 204.8, 208.0, 210.2]
Try it yourself
Recent orders with maxlen
The cafe screen shows the last five orders. Write recent(orders, n=5) that returns a list of the last n orders, most recent last, by feeding the orders one at a time into a deque(maxlen=n).
For orders 1 to 8 and n=5 the result is [4, 5, 6, 7, 8]. With fewer than n orders, return them all.
def recent(orders, n=5):
return orders[:n]
print(recent([1, 2, 3, 4, 5, 6, 7, 8]))Simulating the cafe line
Queues are the heart of simulations. Rosa wants to know whether one barista can keep up with the morning rush, or whether customers will wait too long. A simple simulation steps through the morning one minute at a time: customers arriving in that minute join a queue, and the barista takes the next customer from the front whenever the previous drink is done. The program records how long each customer waited.
from collections import deque
def simulate(arrivals, serve_minutes):
"""arrivals: minute each customer arrives (sorted); returns each customer's wait."""
line, waits = deque(), []
pending = deque(arrivals)
busy_until, minute = 0, 0
while pending or line:
while pending and pending[0] == minute:
line.append(pending.popleft())
if line and minute >= busy_until:
arrived = line.popleft()
waits.append(minute - arrived)
busy_until = minute + serve_minutes
minute += 1
return waits
rush = [0, 0, 1, 2, 2, 2, 3, 5, 6, 6] # ten customers over six minutes
for serve in (1, 2):
w = simulate(rush, serve)
print(f"{serve} min per drink: average wait {sum(w) / len(w):.1f} min, longest {max(w)} min")1 min per drink: average wait 1.8 min, longest 3 min 2 min per drink: average wait 6.3 min, longest 12 min
The simulation shows that at one minute per drink the line clears quickly, while at two minutes the waits pile up. Simulations like this, with random arrivals instead of a fixed list, help businesses plan staffing. Python's standard library also has queue.Queue, a queue that several threads can share safely, which PYTH-250 uses for concurrent programs; for ordinary single-threaded code, deque is simpler and faster.
Worked problem
Choosing between a stack and a queue
For each task, decide whether a stack or a queue fits: (a) customer support emails answered in the order received; (b) a web browser's Back button; (c) roast batches loaded into the cooling tray and removed in the order loaded; (d) checking that HTML tags are properly nested.
Show solution
- (a) Emails should be answered oldest first: a queue (FIFO).
- (b) Back returns to the most recently visited page: a stack (LIFO).
- (c) First loaded, first removed: a queue.
- (d) The most recently opened tag must close first: a stack, exactly like matching brackets.
Answer: Queues for (a) and (c), stacks for (b) and (d).
Common mistakes
MistakeUsing list.pop(0) to take from the front of a queue.
InsteadUse collections.deque and popleft(), which is O(1).
MistakePopping from an empty stack or queue without checking.
InsteadCheck len() first, or catch IndexError and handle it.
MistakeForgetting to clear the redo stack after a new change.
InsteadClear redo whenever a fresh change is recorded.
MistakeIndexing into the middle of a long deque.
InsteadUse a deque for its ends; use a list when you need random access.
MistakeDeclaring brackets balanced when the stack is not empty at the end.
InsteadReturn True only if no mismatch occurred and the stack is empty.
MistakeRe-summing a whole window on every new reading.
InsteadKeep a running total, subtracting the item that falls off.
Summary and key terms
- An abstract data type is defined by its operations; the implementation is a separate choice.
- A stack is last in, first out: push, pop and peek at the top. A list with append and pop() is an ideal stack.
- Stacks power undo and redo, bracket and tag matching, and the call stack behind function calls.
- A queue is first in, first out: enqueue at the back, dequeue at the front.
- collections.deque adds and removes at both ends in O(1); list.pop(0) is O(n).
- deque(maxlen=n) keeps the most recent n items, which makes sliding windows simple.
- Queues model waiting lines, so they are central to simulations.
- abstract data type
- A data structure described by its operations and their rules, not its storage.
- stack
- A collection where the last item added is the first removed (LIFO).
- push
- Add an item to the top of a stack.
- pop
- Remove and return the top item of a stack.
- peek
- Look at the next item to be removed without removing it.
- queue
- A collection where the first item added is the first removed (FIFO).
- enqueue
- Add an item to the back of a queue.
- dequeue
- Remove the item at the front of a queue.
- deque
- A double-ended queue with O(1) operations at both ends.
- sliding window
- The most recent fixed number of items in a stream.
Practice problems
Work each problem on paper first, then reveal the answer to check.
Push A, B, C onto a stack, pop once, then push D. What is on top?
Show answer
D, with B under it and A at the bottom.
Enqueue A, B, C, dequeue once, then enqueue D. What is at the front?
Show answer
B.
Why is a list a good stack but a poor queue?
Show answer
append and pop() at the end are O(1), but pop(0) at the front is O(n).
Trace balanced("{[()]}") step by step. What does it return?
Show answer
Push {, [, (; then ) matches (, ] matches [, } matches {; the stack is empty, so True.
What does deque([1, 2, 3], maxlen=3).append(4) leave in the deque?
Show answer
deque([2, 3, 4]).
Which structure suits a printer handling jobs in the order sent?
Show answer
A queue.
Lesson quiz · 5 questions
Score 60% or more to complete this lesson.
Create a free account to save your progress, take quizzes and earn a certificate.