The queue module provides synchronized queues for passing work between threads. It removes the need to wrap a shared list in home-grown locks and makes producer backpressure explicit.
from queue import Queue
from threading import Thread
fila: Queue[int | None] = Queue(maxsize=100)
def consumidor() -> None:
while True:
item = fila.get()
try:
if item is None:
return
print(item * 2)
finally:
fila.task_done()
worker = Thread(target=consumidor)
worker.start()
for valor in range(5):
fila.put(valor)
fila.put(None)
fila.join()
worker.join()
put() waits when a bounded queue is full, while get() waits for an item. Every retrieved item requires task_done(), including failed work, which is why finally matters. join() waits until the unfinished-task count reaches zero.
Practical guidance
Choose Queue for FIFO, LifoQueue for stack order, and PriorityQueue for priority tuples. Do not use qsize() to make a concurrency decision because the size can change before the next instruction. Define shutdown deliberately with one sentinel per consumer or the shutdown API supported by your Python version.
Compare execution models in Python multithreading and multiprocessing. For event-loop code, use the async-specific tools in the async and await guide.
The official queue module documentation, accessed July 22, 2026, documents the API and its guarantees.
Why a queue beats a shared list
A list guarded by a Lock can work, but waiting, notification, capacity, and shutdown soon become application code. Polling with if items: wastes CPU or adds latency. Queue combines storage, locking, and condition signaling in a tested abstraction. Producers and consumers can operate concurrently without corrupting the container.
That guarantee covers the handoff, not every side effect. If consumers update the same dictionary, file, or mutable object, that resource still needs its own synchronization.
Backpressure with maxsize
An unbounded queue lets fast producers consume memory while a destination is slow. With Queue(maxsize=20), the next put() waits until capacity is available. This propagates pressure upstream and limits outstanding work. Pick a capacity from item size, acceptable memory, burst length, and processing latency rather than an arbitrary large number.
put_nowait() and get_nowait() raise Full or Empty. They fit an explicit policy such as dropping low-value telemetry. Do not first inspect full() or empty() and assume the result remains true: another thread can change the queue before the next instruction.
from queue import Full, Queue
events: Queue[str] = Queue(maxsize=2)
def publish(event: str) -> bool:
try:
events.put(event, timeout=0.5)
return True
except Full:
return False
A timeout prevents an infinite wait and gives the caller a chance to log, retry, or cancel.
Multiple consumers and shutdown
When sentinels are used, send one per consumer. A single sentinel stops only the thread that receives it. Choose a value that cannot be mistaken for valid work. None is fine if real items are never null; a private object() is safer in generic code.
from queue import Queue
from threading import Thread
STOP = object()
jobs: Queue[object] = Queue()
def run() -> None:
while True:
job = jobs.get()
try:
if job is STOP:
return
process(job)
except Exception:
record_failure(job)
finally:
jobs.task_done()
workers = [Thread(target=run, name=f"worker-{i}") for i in range(3)]
for worker in workers:
worker.start()
for item in load_jobs():
jobs.put(item)
for _ in workers:
jobs.put(STOP)
jobs.join()
for worker in workers:
worker.join()
Queue.join() waits for every item to receive task_done(). Thread.join() waits for a thread to exit. Using both confirms that the work is accounted for and no worker remains alive.
Errors and unfinished tasks
Each put() increments the unfinished-task counter. task_done() decrements that counter; it does not remove an item. Too many calls raise ValueError, while a missing call can block join() forever. Put it in finally, but only after a successful get().
Choose what failures mean. An uncaught exception kills a consumer and may leave the pipeline stalled. Production code can log context, put failed items on a separate queue, or retry with a strict limit. Blindly requeueing an item can create an endless loop and prevent clean shutdown.
FIFO, LIFO, and priority
Queue provides FIFO behavior and is the usual default. LifoQueue returns recent items first, which can help tree exploration but may starve older jobs. PriorityQueue returns the smallest item. Plain (priority, payload) tuples fail when equal priorities force Python to compare non-orderable payloads.
from dataclasses import dataclass, field
from queue import PriorityQueue
from typing import Any
@dataclass(order=True)
class Job:
priority: int
sequence: int
payload: Any = field(compare=False)
pending: PriorityQueue[Job] = PriorityQueue()
pending.put(Job(2, 0, {"kind": "report"}))
pending.put(Job(1, 1, {"kind": "alert"}))
The sequence number gives stable tie-breaking without comparing dictionaries. Document whether a smaller number represents greater urgency.
Queue, SimpleQueue, asyncio, and processes
SimpleQueue is an unbounded FIFO with a smaller API. Use it when capacity, task_done(), and task join() are unnecessary. asyncio.Queue coordinates coroutines in one event loop and its methods are awaited; it is not a general thread communication primitive. multiprocessing.Queue serializes values across process boundaries and has different performance and shutdown concerns.
Threads commonly improve I/O-bound workloads. CPU-heavy Python code may be constrained by the GIL, making processes a better fit. A queue organizes flow but does not decide the correct concurrency model.
Testing and observability
Use small capacities in tests to exercise backpressure. Add timeouts so a defect becomes a clear failure instead of a hanging suite. Cover successful work, consumer exceptions, a full queue, and shutdown with several workers. Avoid asserting which consumer receives a particular item because thread scheduling is nondeterministic.
In production, measure processed items, failures, duration, and wait time. qsize() can be an approximate metric even though it is unsafe as a synchronization decision. Sustained growth means arrival rate exceeds consumer capacity. Reduce intake, increase workers within downstream limits, or optimize processing.
Implementation checklist
Bound the queue when producers can outrun consumers. Pair every successful get() with exactly one task_done(). Define cancellation and shutdown before starting threads. Catch worker failures with enough context for diagnosis. Finally, document delivery semantics: an in-memory Queue provides coordination, not persistence, transactions, or recovery after the process exits.