Thread-Safe Queues (queue)

Reviewed & published by Brayan K

A queue.Queue is a thread-safe container for passing items between threads — producers put() work in and consumers get() it out, with all the locking handled for you.

Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

It's the backbone of the producer/consumer pattern: one or more threads generate work, others process it, and the queue safely connects them. No manual locks required.

What You'll Learn in This Lesson

1 The Basics: put() and get()

A FIFO queue.Queue hands items back in the order they went in. put(item) adds; get() removes and returns the oldest item; empty() and qsize() report state:

import queue

q = queue.Queue()
for item in ["task-a", "task-b", "task-c"]:
    q.put(item)

print("Size:", q.qsize())          # Size: 3
while not q.empty():
    print("Processing", q.get())   # FIFO: a, then b, then c
print("Done")
import queue

q = queue.Queue()
for item in ["task-a", "task-b", "task-c"]:
    q.put(item)

print("Size:", q.qsize())
while not q.empty():
    print("Processing", q.get())
print("Done")

# ✅ Expected output:
# Size: 3
# Processing task-a
# Processing task-b
# Processing task-c
# Done

2 Producer/Consumer with Threads

Here's the real power: a producer feeds items in while consumer threads pull them out. task_done() marks each item complete, join() waits until everything is processed, and a None "poison pill" tells each worker to stop:

import queue
import threading

q = queue.Queue()
results = []
lock = threading.Lock()

def consumer():
    while True:
        item = q.get()
        if item is None:          # poison pill -> stop
            q.task_done()
            break
        with lock:
            results.append(item * 10)
        q.task_done()             # mark this item handled

workers = [threading.Thread(target=consumer) for _ in range(3)]
for w in workers:
    w.start()

for n in range(1, 6):
    q.put(n)                      # produce work

q.join()                          # wait until queue is drained

for _ in workers:
    q.put(None)                   # one poison pill per worker
for w in workers:
    w.join()

print(sorted(results))
# [10, 20, 30, 40, 50]
import queue
import threading

q = queue.Queue()
results = []
lock = threading.Lock()

def consumer():
    while True:
        item = q.get()
        if item is None:
            q.task_done()
            break
        with lock:
            results.append(item * 10)
        q.task_done()

workers = [threading.Thread(target=consumer) for _ in range(3)]
for w in workers:
    w.start()

for n in range(1, 6):
    q.put(n)

q.join()

for _ in workers:
    q.put(None)
for w in workers:
    w.join()

# sort for deterministic output (workers finish in any order)
print(sorted(results))

# Expected output:
# [10, 20, 30, 40, 50]

3 LifoQueue and PriorityQueue

The queue module ships two ordered variants. A LifoQueue is a stack (last in, first out). A PriorityQueue serves the smallest item first — put (priority, value) tuples and the lowest priority number wins:

import queue

# LifoQueue = stack (last in, first out)
lifo = queue.LifoQueue()
for x in [1, 2, 3]:
    lifo.put(x)
print("LIFO order:", [lifo.get() for _ in range(3)])   # [3, 2, 1]

# PriorityQueue = smallest priority served first
pq = queue.PriorityQueue()
pq.put((2, "medium"))
pq.put((1, "high"))
pq.put((3, "low"))
order = [pq.get()[1] for _ in range(3)]
print("Priority order:", order)   # ['high', 'medium', 'low']
import queue

lifo = queue.LifoQueue()
for x in [1, 2, 3]:
    lifo.put(x)
print("LIFO order:", [lifo.get() for _ in range(3)])

pq = queue.PriorityQueue()
pq.put((2, "medium"))
pq.put((1, "high"))
pq.put((3, "low"))
order = [pq.get()[1] for _ in range(3)]
print("Priority order:", order)

# ✅ Expected output:
# LIFO order: [3, 2, 1]
# Priority order: ['high', 'medium', 'low']

🎯 Your Turn: Fill in the Blanks

Replace each ___ so the queue is filled, then fully drained into a list while marking each item done.

import queue

q = queue.Queue()
for word in ["red", "green", "blue"]:
    q.___(word)               # hint: add to the queue

collected = []
while not q.empty():
    collected.append(q.___())  # hint: remove from the queue
    q.task_done()

print(collected)

# Expected output:
# ['red', 'green', 'blue']

⚠️ Common Errors

❌ Calling get() on an empty queue with no thread to fill it

q = queue.Queue()
print(q.get())        # hangs forever — nothing to get

✅ Guard with empty(), or use a timeout

if not q.empty():
    print(q.get())
# or: q.get(timeout=1)  -> raises queue.Empty instead of hanging

❌ Forgetting task_done(), so join() never returns

item = q.get()
process(item)
# missing q.task_done() -> q.join() blocks forever

✅ Always pair get() with task_done()

item = q.get()
process(item)
q.task_done()

❌ Sending fewer poison pills than workers

If you have 3 worker threads, you must put 3 None sentinels — otherwise some workers never receive the stop signal and join() on them hangs.

🎯 Mini Challenge: Square Pipeline

Two worker threads pull numbers from a work queue, square them, and put the results on a second queue. Drain the result queue, sort it, and print the sum.

import queue
import threading

work = queue.Queue()
done = queue.Queue()

def worker():
    while True:
        n = work.get()
        if n is None:
            work.task_done()
            break
        done.put(n * n)
        work.task_done()

threads = [threading.Thread(target=worker) for _ in range(2)]
for t in threads:
    t.start()

for n in [1, 2, 3, 4, 5]:
    work.put(n)
work.join()

for _ in threads:
    work.put(None)
for t in threads:
    t.join()

squares = sorted(done.queue)
print("Squares:", squares)
print("Sum:", sum(squares))

# Expected output:
# Squares: [1, 4, 9, 16, 25]
# Sum: 55

📚 Keep Learning

Frequently Asked Questions

Lesson complete — you can pipe work between threads!

You can put() and get() safely from many threads, coordinate completion with task_done() and join(), stop workers with a poison pill, and choose LifoQueue or PriorityQueue when ordering matters.

Practice quiz

Why use queue.Queue instead of a plain list to pass work between threads?

  • It is faster than a list
  • It uses less memory
  • It is thread-safe — handles locking internally
  • It sorts items automatically

Answer: It is thread-safe — handles locking internally. Queue lets multiple threads put() and get() safely without you writing any locks.

A plain queue.Queue() serves items in what order?

  • FIFO (first in, first out)
  • LIFO (last in, first out)
  • Random
  • Sorted

Answer: FIFO (first in, first out). queue.Queue is FIFO — the oldest item put in comes out first.

After putting 1, 2, 3 into a LifoQueue, what does draining it produce?

  • [1, 2, 3]
  • [2, 1, 3]
  • [1, 3, 2]
  • [3, 2, 1]

Answer: [3, 2, 1]. A LifoQueue is a stack: last in, first out, so you get [3, 2, 1].

A PriorityQueue serves which item first?

  • The most recently added
  • The one with the smallest priority value
  • The one with the largest priority value
  • A random one

Answer: The one with the smallest priority value. Put (priority, value) tuples; the lowest priority number is served first.

What happens if you call get() on an empty Queue with no thread to fill it?

  • It blocks (waits) forever
  • It returns None
  • It raises immediately
  • It returns an empty string

Answer: It blocks (waits) forever. By default get() blocks until an item is available — in single-threaded code that hangs forever.

What does a worker do with each item it finishes to coordinate with join()?

  • q.done()
  • q.finish()
  • q.task_done()
  • q.complete()

Answer: q.task_done(). Each consumer calls q.task_done(); q.join() unblocks once task_done count equals items put.

What does q.join() do?

  • Merges two queues
  • Blocks until every queued item has been marked task_done()
  • Starts all worker threads
  • Empties the queue

Answer: Blocks until every queued item has been marked task_done(). join() waits until the queue is fully drained and processed before returning.

What is a 'poison pill' in the producer/consumer pattern?

  • A corrupted item
  • An exception type
  • A priority of zero
  • A sentinel value (often None) that tells a worker to stop

Answer: A sentinel value (often None) that tells a worker to stop. Workers loop on get(); pulling a None sentinel signals them to break and exit cleanly.

If you have 3 worker threads, how many poison pills must you put?

  • 1
  • 3
  • 2
  • It doesn't matter

Answer: 3. One sentinel per worker — fewer pills means some workers never get the stop signal and join() hangs.

How can you make get() raise instead of hanging on an empty queue?

  • q.get(block=True)
  • q.get(wait=False)
  • q.get(timeout=1)
  • q.get(strict=True)

Answer: q.get(timeout=1). q.get(timeout=1) raises queue.Empty after the timeout instead of blocking forever.

Continue this course

Frequently asked questions

Why use queue.Queue instead of a plain list?

queue.Queue is thread-safe: multiple threads can put() and get() at the same time without corrupting the data or needing your own locks. A plain list is not safe for concurrent writes from several threads. Queue handles all the locking internally, so it's the right tool for passing work between threads.

What do task_done() and join() do?

They let a producer know when all the work it queued has been processed. Each consumer calls q.task_done() after finishing an item. The producer calls q.join(), which blocks until the number of task_done() calls equals the number of items put in. It's how you wait for a work queue to fully drain before moving on.

What's a 'poison pill'?

It's a sentinel value (commonly None) that you put on the queue to tell a consumer thread to stop. Worker threads usually loop forever on q.get(); when they pull a None, they break out of the loop and exit. You send one poison pill per worker so every thread gets the signal to shut down cleanly.

When would I use LifoQueue or PriorityQueue?

Use LifoQueue when you want stack behavior — the most recently added item comes out first (useful for depth-first traversal). Use PriorityQueue when items should come out in priority order rather than arrival order — you put (priority, item) tuples and the smallest priority is served first, perfect for task schedulers.