Threads are the simplest way to overlap slow I/O such as downloads, disk access and API calls.
A thread is an independent flow of execution inside your process. All threads share the same memory: globals, objects, open files.
t = threading.Thread(target=download, args=(url,), name="dl-1")
t.start() # begins running download(url) in the new thread
...
t.join() # wait until it finishesKey points:
start()returns immediately; the function runs in the background. Callingrun()directly would execute it in the current thread, which is a common mistake.join(timeout=None)blocks until the thread ends. Always join threads you start, or know why you don't.- A thread's return value is discarded. Pass results back through a shared structure, a queue, or use an executor (which returns futures).
- Exceptions in a thread don't propagate to the starter; they're printed via
threading.excepthookand the thread dies. threading.current_thread().nameandthreading.get_ident()identify the running thread;threading.active_count()counts live ones.
Threads are cheap compared with processes but not free (each has its own stack). For many short tasks, reuse a fixed set of threads with a ThreadPoolExecutor instead of creating one thread per task.
Example
import threading, time
results = {}
def fetch(name, delay):
time.sleep(delay) # stands in for network I/O
results[name] = f"{name} done by {threading.current_thread().name}"
start = time.perf_counter()
threads = [threading.Thread(target=fetch, args=(f"job{i}", 0.5), name=f"worker-{i}")
for i in range(3)]
for t in threads:
t.start()
for t in threads:
t.join()
print(results["job1"]) # job1 done by worker-1
print(f"{time.perf_counter() - start:.1f}s") # 0.5s, not 1.5sRemember
- Thread(target=f, args=(...)).start() runs f concurrently
- join() waits for the thread to finish
- Return values and exceptions don't come back automatically
Watch out for
- Calling t.run() instead of t.start(), which runs the function synchronously in the current thread.