The essentials

Quick reference

One focused task per row. Jump to the related section for complete, working examples.

UseSyntaxExamples
Start a threadworker.start()View examples
Wait with a deadlineworker.join(timeout=2.0)View examples
Name a workerworker = threading.Thread(name='indexer', target=build_index)View examples
Guard a critical sectionwith lock: update_shared_state()View examples
Attempt bounded acquisitionacquired = lock.acquire(timeout=0.5)View examples
Create a reentrant locklock = threading.RLock()View examples
Wait for a predicateready = condition.wait_for(predicate, timeout=1.0)View examples
Notify one waitercondition.notify()View examples
Request cooperative stopstop_event.set()View examples
Limit concurrent entrieslimit = threading.Semaphore(4)View examples
Detect excess releaselimit = threading.BoundedSemaphore(value=4)View examples
Create a phase barrierbarrier = threading.Barrier(parties=3, timeout=5.0)View examples
Arrive at a barrierindex = barrier.wait()View examples
Abort a barrierbarrier.abort()View examples
Create backpressurejobs = queue.Queue(maxsize=100)View examples
Acknowledge a queued taskjobs.task_done()View examples
Wait for task acknowledgementsjobs.join()View examples
Check completion after joinif worker.is_alive(): handle_timeout()View examples
Install an exception observerthreading.excepthook = report_thread_failureView examples

Threads share a process and its objects, which makes communication inexpensive and races possible. This guide treats ownership, bounded waiting, exception propagation, and shutdown as part of the design. On ordinary CPython builds the GIL does not make compound operations thread-safe; threads are usually best for overlapping blocking I/O. Free-threaded builds can execute Python code in parallel, increasing the importance of explicit synchronization.

Step by step

Detailed examples

01

Start, identify, and join non-daemon threads

Pass immutable inputs through args, give operational threads useful names, and retain each Thread so the owner can join it. start() may be called once; join() may be called repeatedly, but joining the current thread raises RuntimeError. Daemon threads are stopped abruptly during interpreter shutdown, so they are unsuitable for work that must flush or release resources.

Coordinate a worker with join
import threading

result: list[int] = []
worker = threading.Thread(target=lambda: result.append(21 * 2), name="calculator")
worker.start()
worker.join()
print(worker.name)
print(result[0])
Output
calculator
42
Back to quick reference ↑
02

Protect invariants with Lock and RLock

A lock protects an invariant, not merely a line of code. Hold it for the smallest complete state transition and use with so exceptions cannot strand it. Lock has no owner and is non-reentrant; RLock tracks its owning thread and recursion depth, which is useful when synchronized methods call one another but can conceal an overly tangled locking design.

Update related values atomically
import threading

lock = threading.Lock()
state = {"accepted": 0, "total": 0}

def record(value: int) -> None:
    with lock:
        state["accepted"] += 1
        state["total"] += value

threads = [threading.Thread(target=record, args=(value,)) for value in (2, 3, 5)]
for thread in threads:
    thread.start()
for thread in threads:
    thread.join()
print(state)
Output
{'accepted': 3, 'total': 10}
Back to quick reference ↑
03

Wait for state with predicates and events

Condition combines a lock with a wait set. Always wait in a loop or use wait_for(predicate), because wakeups do not prove that the state is ready. Mutate the predicate while holding the condition lock, then notify. Event is a simpler level-triggered flag for readiness or cooperative stop requests; it does not carry data and set() wakes all current waiters.

Publish state before notification
import threading

condition = threading.Condition()
items: list[str] = []

def consumer() -> None:
    with condition:
        condition.wait_for(lambda: bool(items))
        print(items.pop())

thread = threading.Thread(target=consumer)
thread.start()
with condition:
    items.append("ready")
    condition.notify()
thread.join()
Output
ready
Back to quick reference ↑
04

Bound concurrency with semaphores

Semaphore represents a permit count and is appropriate for limiting simultaneous access to a finite resource. BoundedSemaphore additionally raises ValueError when released too many times, catching accounting defects. A semaphore is not a task queue and does not guarantee fair waiter ordering; pair it with explicit work ownership and deadlines.

Use a bounded permit as a context manager
import threading

limit = threading.BoundedSemaphore(1)
with limit:
    print("inside")
print("released")
Output
inside
released
Back to quick reference ↑
05

Coordinate fixed phases with Barrier

Barrier releases a fixed number of participants after every participant calls wait(). The returned indexes are unique from zero through parties minus one and can elect one thread for housekeeping. A timeout, exception in the action, reset, or abort can break the barrier; every participant must handle BrokenBarrierError rather than wait indefinitely.

Release two phase participants
import threading

barrier = threading.Barrier(2)
indexes: list[int] = []

def arrive() -> None:
    indexes.append(barrier.wait())

thread = threading.Thread(target=arrive)
thread.start()
indexes.append(barrier.wait())
thread.join()
print(sorted(indexes))
Output
[0, 1]
Back to quick reference ↑
06

Transfer ownership through Queue

queue.Queue provides synchronized put/get operations and optional capacity. Each successful get() representing a task must have exactly one task_done(), normally in finally, so join() can observe completion. Python 3.13 added Queue.shutdown(); use sentinel values when supporting older runtimes. threading.local stores per-thread attributes but should not replace explicit request context at API boundaries.

Finish queued work with a sentinel
from queue import Queue
import threading

jobs: Queue[int | None] = Queue()
answers: list[int] = []

def consume() -> None:
    while True:
        item = jobs.get()
        try:
            if item is None:
                return
            answers.append(item * item)
        finally:
            jobs.task_done()

thread = threading.Thread(target=consume)
thread.start()
for item in (2, 3):
    jobs.put(item)
jobs.put(None)
jobs.join()
thread.join()
print(answers)
Output
[4, 9]
Back to quick reference ↑
07

Make cancellation and failure observable

Threads cannot be safely killed from the outside. Use an Event plus bounded blocking calls, and join every worker during orderly shutdown. Thread exceptions normally reach threading.excepthook rather than the creating thread; wrap worker entry points to publish failures, or use concurrent.futures when result and exception propagation is the primary need. Never hold a lock while joining a thread that may need that lock.

Return a worker failure to its owner
from queue import Queue
import threading

outcomes: Queue[tuple[str, str]] = Queue()

def run() -> None:
    try:
        raise ValueError("invalid job")
    except Exception as error:
        outcomes.put((type(error).__name__, str(error)))

thread = threading.Thread(target=run)
thread.start()
thread.join()
print(outcomes.get())
Output
('ValueError', 'invalid job')
Back to quick reference ↑

Local code tester

Model cooperative cancellation

Run a deterministic worker loop that obeys an explicit stop predicate, the same ownership model used with threading.Event.

Runs in your browser
Output
Press Run to load Python locally.

Sources and further reading

References

Authoritative documentation used to verify and expand this cheat sheet.

  1. Python Software Foundationthreading — Thread-based parallelismdocs.python.org
  2. Python Software Foundationqueue — A synchronized queue classdocs.python.org
  3. Python Software FoundationThread states and the global interpreter lockdocs.python.org
  4. Python Software FoundationPEP 703 — Making the Global Interpreter Lock Optionalpeps.python.org
  5. Python Software Foundationconcurrent.futures — Launching parallel tasksdocs.python.org

Help us improve

Found a typo or missing example?

Tell us what would make this cheat sheet clearer, more complete, or more useful.

Share feedback