What is the difference between ThreadPoolExecutor and ProcessPoolExecutor?
Both ThreadPoolExecutor and ProcessPoolExecutor are part of Python's concurrent.futures module and provide a high-level interface for running callables asynchronously using a pool of workers. The key difference is the unit of concurrency and how Python's GIL affects them.
ThreadPoolExecutor creates a pool of OS threads within the same process. All threads share the same memory space, the same Python interpreter state, and the same GIL. Because of the GIL, only one thread executes Python bytecode at a time. This means ThreadPoolExecutor does not achieve CPU parallelism for CPU-bound tasks -- threads take turns. However, the GIL is released during I/O operations (network, disk, subprocess calls) and during many C extension calls. For I/O-bound workloads -- making hundreds of concurrent HTTP requests, reading files, querying databases -- ThreadPoolExecutor is effective and has low overhead. Shared memory means threads can directly access shared Python objects, but this requires careful synchronization.
ProcessPoolExecutor creates a pool of separate OS processes. Each process has its own Python interpreter, its own GIL, and its own memory space. This means multiple CPU cores can run Python bytecode in parallel simultaneously. CPU-bound tasks -- numerical processing, data transformation, cryptographic operations -- run genuinely concurrently. The trade-offs are significant: process startup is expensive (tens to hundreds of milliseconds each), and all data communicated between the main process and workers must be serialized via pickle. Functions passed to the pool must be picklable (no lambdas, no closures over non-picklable objects). The memory overhead is high because each process is a full Python runtime.
The practical decision rule: use ThreadPoolExecutor for I/O-bound work where tasks spend most of their time waiting. Use ProcessPoolExecutor for CPU-bound work where tasks actively compute. Use neither and reach for asyncio if your I/O-bound work is highly concurrent and you want lower overhead than threads. A common production pattern is asyncio for the event loop with a ThreadPoolExecutor via loop.run_in_executor for blocking I/O calls that lack async APIs.
| Aspect | ThreadPoolExecutor | ProcessPoolExecutor |
|---|---|---|
| Unit of concurrency | OS threads (same process) | OS processes (separate) |
| GIL impact | One thread runs Python at a time | Each process has its own GIL -- true parallelism |
| CPU-bound tasks | No speedup -- GIL serializes bytecode | Speedup proportional to core count |
| I/O-bound tasks | Effective -- GIL released during I/O | Works but higher overhead than threads |
| Memory | Shared memory space | Separate memory per process |
| Startup cost | Low -- thread creation is cheap | High -- process spawn + import overhead |
| Data passing | Direct shared memory | Pickle serialization -- all args and results |
Candidate knows threads share memory and the GIL while processes do not, and that ProcessPoolExecutor is for CPU-bound work. Cannot explain pickle serialization overhead, asyncio as an alternative, or the implications of shared memory for thread safety.
Candidate precisely explains GIL impact on both, names the correct use case for each, mentions pickle serialization overhead and picklability constraints for ProcessPoolExecutor, discusses thread safety implications of shared memory, and mentions asyncio + run_in_executor as a common hybrid pattern.
Reading the answer is step one. Explaining it unprompted — under interview pressure — is what actually matters. Get AI-graded feedback on your answer with follow-up probes on your weak points.
Get Graded — Free Assessment