flowchart LR
A["producer thread"] --> B["q.put(item)"]
B --> C{"maxsize reached?"}
C -->|yes| D["producer BLOCKS -- back-pressure"]
C -->|no| E["item queued"]
E --> F["q.get() in a worker"]
F --> G{"empty?"}
G -->|yes| H["worker BLOCKS -- no polling, no spinning"]
G -->|no| I["process it"]
I --> J["q.task_done()"]
J --> K["q.join() returns when every put has a matching task_done"]
L["a sentinel value"] --> M["is how workers are told to stop"]
Why Queue?
queue.Queue is thread-safe.
Use it to:
- pass tasks from producer threads to worker threads
- avoid manual lock management
Producer-consumer example
import threading
import queue
import time
q = queue.Queue()
def producer():
for i in range(5):
q.put(i)
print("produced", i)
q.put(None) # sentinel
def consumer():
while True:
item = q.get()
try:
if item is None:
break
print("consuming", item)
time.sleep(0.2)
finally:
q.task_done()
t1 = threading.Thread(target=producer)
t2 = threading.Thread(target=consumer)
t1.start(); t2.start()
t1.join()
q.join() # wait until all tasks are marked done
print("All tasks processed")Tips
- Use a sentinel (
None) to stop consumers. - Always call
task_done()(often in afinally).
Why threads need a safe channel
Threads share memory, which makes it easy to pass data around and equally easy to corrupt it. Sharing a plain list means guarding every read and write with a lock. queue.Queue does that work for you: put and get are already thread-safe, and get waits politely when the queue is empty, so a consumer never spins in a busy loop.
Following the example
The producer puts five numbers on the queue and then a None sentinel. The consumer loops on q.get(), which blocks until an item is available. For each real item it prints, does some work, and calls task_done() in a finally block. When it receives None it breaks out. In the main thread, t1.join() waits for the producer, and q.join() waits until every item, including the sentinel, has been marked done. The output lines from producer and consumer may interleave differently on each run, which is normal.
Several workers sharing a job list
A queue also spreads work across several consumers. Each item goes to exactly one consumer:
import queue
import threading
jobs: queue.Queue = queue.Queue()
results: queue.Queue = queue.Queue()
def worker() -> None:
while True:
n = jobs.get()
if n is None:
jobs.task_done()
return
results.put(n * n)
jobs.task_done()
threads = [threading.Thread(target=worker) for _ in range(3)]
for t in threads:
t.start()
for n in range(10):
jobs.put(n)
for _ in threads:
jobs.put(None) # one stop signal per worker
jobs.join()
for t in threads:
t.join()
print(sorted(results.get() for _ in range(results.qsize())))The output is [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]. Sorting makes it independent of which worker finished first.
Useful variations
Queue(maxsize=10)makesputwait when full, which applies backpressure to fast producers.q.get(timeout=1)raisesqueue.Emptyinstead of waiting forever.q.get_nowait()andq.put_nowait()never block.queue.LifoQueueandqueue.PriorityQueuechange the order items come out.
Common mistakes
- Forgetting
task_done(), sojoin()never returns. - Too few sentinels for the number of consumers.
- Using a plain list shared between threads without a lock.
- Using
qsize()orempty()to make decisions. Another thread may change the queue right after you check.
When to use it
Use a queue whenever one thread produces work and others consume it: downloaders feeding a parser, a logger thread collecting messages, or a pool of workers processing jobs.
Check yourself
A consumer calls `q.get()` on an empty `queue.Queue`. What happens?
It parks, using no CPU, until something is put. `get_nowait()` is the version that raises `Empty` instead.
Show answer
C — It blocks until an item arrives — It parks, using no CPU, until something is put. `get_nowait()` is the version that raises `Empty` instead.
What does `maxsize` give you beyond a memory limit?
With `maxsize=0` (unbounded) a producer faster than its consumer will grow the queue until memory runs out. A bound makes the producer wait instead.
Show answer
B — Back-pressure — a full queue blocks the producer — With `maxsize=0` (unbounded) a producer faster than its consumer will grow the queue until memory runs out. A bound makes the producer wait instead.
What is the point of `task_done()` and `join()`?
`join()` returns when every `put` has a matching `task_done`. Note it tracks completion of the WORK, not merely that the queue is empty.
Show answer
B — Knowing when every item that was put has been fully handled — `join()` returns when every `put` has a matching `task_done`. Note it tracks completion of the WORK, not merely that the queue is empty.
How do you tell worker threads to stop?
Each worker takes one sentinel and exits, so every worker gets exactly one. A queue has no close, and killing threads is not possible.
Show answer
B — Put one sentinel value per worker on the queue — Each worker takes one sentinel and exits, so every worker gets exactly one. A queue has no close, and killing threads is not possible.
🧪 Try It Yourself
Exercise 1 – Put and Get
Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.
Press Run to see what your code prints.Exercise 2 – Producer-Consumer
Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.
Press Run to see what your code prints.Exercise 3 – Queue Size
Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.
Press Run to see what your code prints.