Creating and Joining Threads
Creating threads is a one-liner, but the discipline around starting, joining, and cleaning up is where bugs appear. This chapter shows the standard patterns: passing arguments, running several threads, joining them in order, and propagating results or exceptions back to the caller.
Passing Arguments
import threading
import time
def worker(name, duration):
print(f"{name} started")
time.sleep(duration)
print(f"{name} finished")
threads = []
for i in range(3):
t = threading.Thread(target=worker, args=(f"T{i}", 1), name=f"Worker-{i}")
t.start()
threads.append(t)
for t in threads:
t.join()
print("all threads joined")
Keyword arguments go through the kwargs dict: threading.Thread(target=fn, kwargs={"retries": 3}).
Capturing Results
Threads have no return value. To get results back, hand the thread a container it can fill, or use concurrent.futures (covered later), which packages this pattern for you:
import threading
results = {}
def compute(key, value):
results[key] = value * 2
ts = [threading.Thread(target=compute, args=(k, v)) for k, v in {"a": 1, "b": 2}.items()]
[t.start() for t in ts]
[t.join() for t in ts]
print(results) # {'a': 2, 'b': 4}
Exception Handling
An exception in a thread prints a traceback and kills that thread; it does not crash the program or notify the main thread. If a failure must be visible, catch it inside the target and store it on a shared object:
import threading
class Worker(threading.Thread):
def __init__(self, fn):
super().__init__()
self.fn = fn
self.error = None
def run(self):
try:
self.fn()
except Exception as exc:
self.error = exc
w = Worker(lambda: 1 / 0)
w.start()
w.join()
if w.error:
print("worker failed:", w.error)
Join with a Timeout
join(timeout=5) returns after five seconds even if the thread is still running, so the main thread can check is_alive() and decide whether to proceed, warn, or abandon the work. A plain join() without a timeout blocks forever, which is fine for short jobs and dangerous for network calls that can hang.
Summary
Start threads with start(), wait with join(), pass data in or out through shared containers, and remember that thread exceptions do not propagate - catch them inside the target when the failure matters.