skip to content

When would you pick queue.SimpleQueue, LifoQueue or PriorityQueue over a plain queue.Queue?

level: middleimportance: nice to knowfreq 22%

answer

  1. Three siblings and one deliberate outsider
  2. One of them is a stack
  3. Smallest comparison wins, not largest
  4. Equal priorities make it compare the payload
  5. The minimal one gives up the bookkeeping

basics

~20 s

All four are thread-safe channels. LifoQueue serves newest first, PriorityQueue serves the lowest-comparing entry first, and SimpleQueue is a minimal unbounded FIFO with no maxsize, task_done or join whose put never blocks. Plain Queue is the default FIFO.

solid answer

~40 s

`queue.Queue` is FIFO; `queue.LifoQueue` is the same class with stack ordering, useful when the freshest item is the most valuable — a depth-first crawl frontier, for instance. `queue.PriorityQueue` keeps a heap and `get()` returns the lowest-comparing entry, so entries are conventionally tuples like `(priority, payload)`; if two priorities tie, the heap compares the payloads, which raises `TypeError` for anything unorderable, so add a monotonic counter as a tiebreaker. All three share `maxsize`, `task_done()` and `join()`. `queue.SimpleQueue` is a different, minimal class: unbounded, implemented in C, with no `maxsize`, no `task_done()`, no `join()` and (as of 3.14) no `shutdown()`. Its `put()` never blocks and is reliable from awkward contexts such as a `__del__` or a signal handler, which is its real reason to exist.

code

python · 22 lines
python
import queue

pq = queue.PriorityQueue()
pq.put((2, "transcode"))
pq.put((1, "probe"))
print(pq.get())                       # (1, 'probe') - lowest tuple wins

tie = queue.PriorityQueue()
tie.put((1, {"clip": "a.mkv"}))
try:
    tie.put((1, {"clip": "b.mkv"}))   # equal priority -> compares the dicts
except TypeError as exc:
    print("unorderable payload:", exc)

lifo = queue.LifoQueue()
lifo.put("first")
lifo.put("second")
print(lifo.get())                     # 'second' - stack order

simple = queue.SimpleQueue()          # unbounded, no maxsize/task_done/join
simple.put("event")
print(simple.get(), simple.qsize())

go deeper

for a junior

Recall the four names and their ordering: queue.Queue is FIFO, queue.LifoQueue is a stack, queue.PriorityQueue serves the lowest-comparing entry, and queue.SimpleQueue is a stripped-down unbounded FIFO.

for a middle

Explain the mechanics: LifoQueue and PriorityQueue are subclasses that change only ordering, priority entries are tuples compared element by element, and SimpleQueue trades maxsize, task_done() and join() for a put() that never blocks.

for a senior

Show judgement about the failure modes — the TypeError on tied priorities and the counter fix, starvation under LifoQueue, and the fact that SimpleQueue offers no backpressure, so choosing it means accepting unbounded growth on that path.

for a principal

Decide when priority ordering belongs in the process at all rather than in a durable broker, and set the convention for entry shape so priority tuples cannot become unorderable as payload types evolve across a codebase.

## One family and one outlier The `queue` module ships four classes. Three of them — `Queue`, `LifoQueue` and `PriorityQueue` — are the same machinery with a different ordering discipline: `LifoQueue` and `PriorityQueue` subclass `Queue` and override only the small hooks that decide how an item is stored and which one comes out next. Everything else is identical, including `maxsize`, the blocking and timeout behaviour of `put()`/`get()`, the `task_done()`/`join()` counter, and `shutdown()`. `SimpleQueue` is not part of that family; it is a deliberately smaller thing. ## LifoQueue: newest first `queue.LifoQueue` serves the most recently put item, so it behaves as a thread-safe stack. It suits work where recency dominates — a depth-first traversal frontier, or a retry buffer where the freshest attempt is the one worth making. Note the fairness cost: under sustained load the items at the bottom may never be served at all, which is fine for a search frontier and unacceptable for user work. ## PriorityQueue: lowest comparison wins `queue.PriorityQueue` keeps its contents as a binary heap and `get()` returns the smallest entry by ordinary Python comparison. Lowest first — a common slip is assuming the highest priority number wins; if you want that, negate the number. The interesting failure is ties. Entries are conventionally `(priority, payload)` tuples, and tuple comparison only looks at the second element when the first elements are equal. Put two entries with the same priority and a payload that does not define an ordering — a dict, or an instance of an ordinary class — and the heap raises `TypeError: '<' not supported between instances of ...`, from inside `put()`, on an input that worked fine yesterday. There are two standard fixes. Insert a monotonically increasing counter as a middle element, `(priority, next(counter), payload)`, which both guarantees comparability and makes ordering stable for equal priorities in insertion order — a heap is not stable on its own. Or use a dataclass declared with `order=True` and mark the payload field `compare=False` so it is excluded from comparison. Note also that the heap only orders the entry at the head correctly; iterating the internal list is not a sorted view. ## SimpleQueue: less is the feature `queue.SimpleQueue` is an unbounded FIFO with a deliberately minimal surface: `put`, `get`, `put_nowait`, `get_nowait`, `empty`, `qsize`. There is no `maxsize`, no `task_done()`, no `join()`, and as of 3.14 no `shutdown()` either. It accepts `block` and `timeout` on `put()` purely for signature compatibility and ignores them, because an unbounded queue can always accept an item. Two things justify choosing it. First, it is implemented in C and does less bookkeeping, so it is measurably faster on a hot path. Second, and more importantly, `put()` never blocks and never needs to acquire a Python-level lock that a reentrant call might already hold. That makes it safe to call from contexts where an ordinary `Queue.put()` is not obviously safe — a `__del__` method or a signal handler, both of which can run in the middle of arbitrary other code on the same thread. The classic use is a logging or event sink that must accept an item from anywhere without any possibility of deadlocking or waiting. The cost is everything it lacks. No `maxsize` means no backpressure — a `SimpleQueue` can grow without bound exactly like an unbounded `Queue`. No `task_done()`/`join()` means no completion barrier, so a coordinator has to count finished work itself. If you need either of those, pick `Queue`. ## What the subclasses inherit Because `LifoQueue` and `PriorityQueue` are ordinary subclasses of `queue.Queue`, everything the parent gained they gained too. `maxsize` bounds all three identically, so a priority queue can apply backpressure just as a FIFO one does. `queue.Full` and `queue.Empty` are raised by the same code paths. The `task_done()`/`join()` counter is inherited unchanged, and so is `shutdown()`, added in 3.13 — calling it on a `PriorityQueue` makes blocked `get()` and `put()` calls raise `queue.ShutDown` exactly as on the base class. That uniformity is why swapping a `Queue` for a `LifoQueue` in an existing pipeline is a one-line change: nothing else about the contract moves. `SimpleQueue` breaks that symmetry, and it is worth being precise about the version story. `shutdown()` arrived on `Queue` in 3.13; on 3.14 `queue.SimpleQueue` still has no `shutdown()` method at all, so a `SimpleQueue` consumer cannot be woken with `queue.ShutDown` and needs a sentinel item or a timed `get()` against a stop flag instead. Assuming the whole module gained `shutdown()` together is an easy and wrong inference. ## Choosing Start with `queue.Queue` and a `maxsize`; it is the right answer for ordinary producer-consumer work between threads. Reach for `LifoQueue` when recency beats fairness, for `PriorityQueue` when some items genuinely must jump the line and you have thought about ties, and for `SimpleQueue` when the producer side must never block or must be callable from a finalizer or signal handler.

  • Two entries in a queue.PriorityQueue have the same priority number. What can go wrong?
    Tuple comparison falls through to the next element, so the heap tries to order the payloads. If they are dicts, sets or ordinary objects without `__lt__`, `put()` raises `TypeError: '<' not supported between instances of ...`. Even when they are comparable, ordering by payload is almost never what you meant. Insert a monotonically increasing counter as a tiebreaker — `(priority, next(counter), payload)` — which also makes equal priorities come out in insertion order, since a heap is not stable.
  • Why does queue.SimpleQueue have no task_done() or join()?
    Because it is deliberately a bare channel. `task_done()`/`join()` require an unfinished-task counter and a condition variable to wait on, which is bookkeeping `SimpleQueue` exists to avoid — it is implemented in C, unbounded, and its `put()` never blocks or waits on a Python-level lock, which is what makes it safe to call from a `__del__` or a signal handler. If you need a completion barrier, or a `maxsize` for backpressure, use `queue.Queue`.
  • Does queue.LifoQueue support maxsize and blocking puts the way queue.Queue does?
    Yes. `LifoQueue` and `PriorityQueue` both subclass `queue.Queue` and override only the hooks that decide storage and retrieval order. `maxsize`, the `block`/`timeout` arguments, `queue.Full` and `queue.Empty`, the `task_done()`/`join()` counter and `shutdown()` all behave identically. The only difference is which item `get()` hands back. `SimpleQueue` is the one that is not a subclass and does not share those features.

saying these in an interview costs you the question

  • Thinks PriorityQueue returns the highest number first
  • Puts unorderable payloads in priority tuples
  • Assumes PriorityQueue is stable for equal priorities
  • Believes SimpleQueue supports maxsize or join
  • Thinks SimpleQueue.put can block when it is busy
  • Calls LifoQueue fair under sustained load

context