Threading lets a program manage multiple flows of work in the same process.
Threads are most useful for tasks that spend time waiting, such as network calls, file operations, or background monitoring.
Threading is not a magic speed boost for CPU-heavy Python code, and shared data needs careful design.
A Thread runs a callable in the same process and shares memory with the creating thread. Pass the callable itself to target, provide positional values through args, and call start exactly once. start schedules the work; calling run directly executes synchronously in the current thread.
join waits for completion and can take a timeout. After a timed join, check is_alive instead of assuming the worker stopped. Python cannot safely force an arbitrary thread to terminate, so long-running workers need cooperative cancellation through an Event or another shared signal.
from threading import Thread
def worker(name):
print(f"working: {name}")
threads = [
Thread(target=worker, args=("download",)),
Thread(target=worker, args=("report",)),
]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
working: download
working: report
The order may vary because threads run independently.
Shared mutable state creates races when a result depends on the order of multiple operations. The GIL in a conventional CPython build does not turn a business operation such as read-modify-write into one atomic transaction, and free-threaded Python builds remove that implicit serialization. Write synchronization that expresses the real invariant.
A Lock protects a short critical section. Acquire it with a with statement so exceptions cannot leave it locked. A Queue is often the cleaner design: producers submit independent messages, consumers process them, and task_done plus join provide completion tracking without exposing a shared list.
from threading import Thread
results = []
def collect(value):
results.append(value.upper())
threads = [Thread(target=collect, args=(word,)) for word in ["csv", "json"]]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
print(sorted(results))
['CSV', 'JSON']
Each thread processes one input, then the main program reads results after join.
queue.Queue provides synchronized put and get operations and can apply backpressure with maxsize. A fixed worker pool consumes items instead of creating an unbounded thread for every request. Send one sentinel per worker during shutdown so every consumer has a defined exit path.
Always pair each successful get with task_done in a finally block. Queue.join then waits until all submitted work has been acknowledged. Decide how failures are reported: a result queue, a Future from concurrent.futures, or a shared error collector guarded by a lock are all clearer than silently printing a traceback.
from queue import Queue
from threading import Thread
jobs = Queue()
results = Queue()
def worker():
while True:
value = jobs.get()
try:
if value is None:
return
results.put(value * value)
finally:
jobs.task_done()
workers = [Thread(target=worker, name=f"worker-{n}") for n in range(2)]
for thread in workers:
thread.start()
for value in (2, 3, 4):
jobs.put(value)
for _ in workers:
jobs.put(None)
jobs.join()
for thread in workers:
thread.join()
print(sorted(results.get() for _ in range(3)))
[4, 9, 16]
The queue owns task coordination, sentinels stop both workers, and sorting makes the displayed result deterministic even though execution order is not.
A worker should check a threading.Event between bounded units of work. Blocking calls also need timeouts; otherwise a cancellation flag cannot be observed. Clearing an Event is appropriate only when the protocol supports restarting. For one-way shutdown, set it once and let ownership code join every worker.
Unhandled exceptions are passed to threading.excepthook, but production code usually needs the failure associated with its submitted task. ThreadPoolExecutor returns Future objects whose result method either returns the value or re-raises the worker exception in the caller. This makes bounded pools, cancellation, and error collection easier than managing raw threads for request-style work.
Threads fit blocking I/O libraries and modest numbers of concurrent operations. asyncio fits many cooperative network tasks when the entire call chain has non-blocking APIs. Processes isolate memory and normally fit CPU-intensive Python work on conventional GIL-enabled CPython. A free-threaded CPython build can execute Python threads in parallel, but extension compatibility and synchronization still need testing.
Measure the real workload before choosing. Record throughput, latency, queue depth, CPU use, memory, and downstream rate limits. Concurrency can reduce waiting time while making overload, retries, shutdown, and observability harder; a sequential implementation remains the correct baseline for comparison.
Try this next
0 of 3 completed
They run concurrently, but exact scheduling depends on the interpreter and operating system. For beginner use, focus on waiting tasks.
Threads are scheduled independently, so one may print before another in different runs.
Learn normal functions first. Then use threading for simple concurrent waiting tasks and async for event-loop style I/O.
Explore 500+ free tutorials across 20+ languages and frameworks.