How to Multi-thread an Operation Within a Loop in Python
Interview Questions practice on Codemia
Over 8,000 real interview questions from top companies, searchable by company and role.
Introduction
When a Python program performs the same I/O-bound operation across many items in a loop, multithreading can significantly reduce total execution time. Instead of processing each item sequentially and waiting for I/O to complete before moving to the next, threads allow overlapping work so that one thread can process while another waits on a network response or file read. Python's threading module and concurrent.futures.ThreadPoolExecutor make this straightforward to implement.
This article shows how to convert a sequential loop into a multithreaded one, covers both low-level threading and the higher-level executor API, and discusses the trade-offs you need to consider.
When Multithreading Helps (and When It Does Not)
Python has a Global Interpreter Lock (GIL) that prevents multiple threads from executing Python bytecode simultaneously. This means threads do not provide true parallelism for CPU-bound work like number crunching or image processing. For CPU-bound tasks, use multiprocessing instead.
However, the GIL is released during I/O operations such as network requests, file reads, database queries, and subprocess calls. Multithreading is effective for these operations because threads can overlap waiting time.
Approach 1: Manual Threading with threading.Thread
The most basic approach creates a thread for each item in the loop.
The join() call blocks the main thread until each worker thread finishes. Without it, the main thread could exit before the workers complete their work.
Approach 2: ThreadPoolExecutor (Recommended)
Creating a raw thread per item is fine for small lists, but it does not scale. If you have 10,000 URLs, spawning 10,000 threads wastes resources and can crash the program. ThreadPoolExecutor solves this by maintaining a fixed pool of worker threads and distributing tasks among them.
With max_workers=5, at most 5 threads run concurrently. The 20 URLs are processed in groups of 5, taking about 4 seconds total instead of 20 seconds sequentially.
Approach 3: Using executor.map for Simple Cases
When you want to apply the same function to every item and collect results in order, executor.map is the most concise option.
Unlike as_completed, map returns results in the same order as the input iterable. It also re-raises exceptions from worker threads when you iterate over the results.
Collecting Results Safely with a Lock
When multiple threads write to a shared data structure, you need synchronization to prevent race conditions.
The with lock block ensures that only one thread appends to results at a time. Without the lock, concurrent appends could corrupt the list or lose items on some Python implementations.
However, if you are using executor.map or as_completed and collecting results from the returned futures, you do not need a lock because each future's result is isolated.
Handling Exceptions in Threads
Exceptions in worker threads are silent by default with manual threading.Thread. They do not propagate to the main thread.
With ThreadPoolExecutor, exceptions are captured and re-raised when you call future.result(). This makes error handling predictable and prevents silent failures.
Choosing the Right Number of Workers
The optimal max_workers value depends on the workload.
For I/O-bound tasks (network requests, file I/O), a good starting point is between 5 and 20 workers. More workers help when each task spends most of its time waiting. Too many workers can overwhelm the target server or exhaust file descriptors.
For CPU-bound tasks (where you should use ProcessPoolExecutor instead), the number of workers should match the CPU core count.
Common Pitfalls
Using threads for CPU-bound work. Due to the GIL, threading does not speed up pure computation in Python. CPU-bound loops should use multiprocessing.Pool or concurrent.futures.ProcessPoolExecutor.
Creating too many threads. Each thread consumes memory for its stack (typically 8 MB on Linux). Creating thousands of threads can exhaust memory. Always use a thread pool with a bounded max_workers.
Ignoring thread safety. Shared mutable state accessed from multiple threads without a lock can produce corrupted data. Use threading.Lock, queue.Queue, or avoid shared state entirely by collecting results through futures.
Not calling join() or using a context manager. If you create threads manually and do not call join(), the main thread may exit while workers are still running. With ThreadPoolExecutor, using the with statement ensures all workers finish before the block exits.
Swallowing exceptions. With raw threading.Thread, exceptions in worker threads are printed to stderr but do not stop the main program. Use ThreadPoolExecutor and check future.result() to catch and handle errors properly.
Summary
To multithread an operation within a loop in Python, use concurrent.futures.ThreadPoolExecutor for most cases. It manages thread lifecycle, limits concurrency, and provides clean error handling through futures. Use executor.map for simple apply-and-collect patterns, and as_completed when you need to process results as they arrive. Reserve manual threading.Thread for cases where you need fine-grained control over thread behavior. Always remember that Python threading is effective for I/O-bound work but does not improve CPU-bound performance due to the GIL.
Related reading
- How to multithread C code
- How to notify user when async task ends in PHP/ Windows
- How to obtain a Thread id in Python?
- How to obtain a Thread id in Python?
- How to normalize a NumPy array to within a certain range?
- How to obtain information gain from a scikit-learn DecisionTreeClassifier?
- How to obtain JNI interface pointer JNIEnv for asynchronous calls
- How to parallelize a training loop ever samples of a batch when CPU is only available in pytorch?
.png&w=3840&q=75)
Tackling System Design Interview Problems
A short course that equips you with the skills to approach system design interviews methodically.
Start the free courseTrack what you have practised
A free account saves your progress, solutions and study plan across every problem on Codemia.
Interview Questions practice on Codemia
Over 8,000 real interview questions from top companies, searchable by company and role.