← Back to Python

ThreadPoolExecutor & ProcessPoolExecutor

PythonSeniorpython

The Question

What is the difference between ThreadPoolExecutor and ProcessPoolExecutor?

What a Strong Answer Covers

  • ThreadPool = I/O
  • "ProcessPool = CPU
  • bypasses GIL
  • "submit() → Future

Senior-Level Answer

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.

Key Differences

AspectThreadPoolExecutorProcessPoolExecutor
Unit of concurrencyOS threads (same process)OS processes (separate)
GIL impactOne thread runs Python at a timeEach process has its own GIL -- true parallelism
CPU-bound tasksNo speedup -- GIL serializes bytecodeSpeedup proportional to core count
I/O-bound tasksEffective -- GIL released during I/OWorks but higher overhead than threads
MemoryShared memory spaceSeparate memory per process
Startup costLow -- thread creation is cheapHigh -- process spawn + import overhead
Data passingDirect shared memoryPickle serialization -- all args and results

What Separates a 2/3 from a 3/3

2/3 — Passing but Incomplete

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.

3/3 — Strong Answer

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.

Common Mistakes

  • Saying ThreadPoolExecutor achieves CPU parallelism -- it does not due to the GIL
  • Forgetting that ProcessPoolExecutor uses pickle -- passing non-picklable objects like lambdas causes runtime errors
  • Not knowing that the GIL is released during I/O, which is why ThreadPoolExecutor is still useful
  • Ignoring asyncio as the better tool for I/O-bound concurrency at high scale

Follow-Up Questions

  • When would you use asyncio instead of ThreadPoolExecutor? — asyncio is better for thousands of concurrent connections with minimal CPU work per request -- it avoids thread creation overhead and context switching at scale.
  • What happens if you pass a lambda to ProcessPoolExecutor? — It raises a PicklingError because lambdas cannot be pickled. Use module-level functions or functools.partial instead.
  • How does the max_workers default differ between the two? — ThreadPoolExecutor defaults to min(32, os.cpu_count() + 4); ProcessPoolExecutor defaults to os.cpu_count().
  • How would you combine asyncio with CPU-bound code? — Use loop.run_in_executor with a ProcessPoolExecutor to offload CPU-bound work to a process pool without blocking the event loop.

Related Questions

  • GIL — What It Is and What It Protects
  • Decorators — Under the Hood
  • Generators & yield
  • Context Managers
  • LEGB Rule

Can You Explain This Cold?

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
GrindQuestionsAITechnical interview assessment
TermsPrivacyAbout