Parallelism
Master Python's concurrent.futures module to build high-performance parallel systems using ThreadPoolExecutor and ProcessPoolExecutor.
Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
- • How concurrent.futures abstracts threads and processes into a unified API
- • When to use ThreadPoolExecutor vs ProcessPoolExecutor
- • How Futures represent pending work — and how to collect results
- • Running parallel tasks with map() and submit()
- • Handling errors, cancellations, and timeouts in parallel jobs
- • Real patterns: parallel image processing, batch API calls, data pipelines
1. What concurrent.futures Actually Does
Executor
Workers Are
Best For
Examples
ThreadPoolExecutor
Threads
I/O-bound tasks (waiting)
API calls, file downloads, DB queries
ProcessPoolExecutor
Processes
CPU-bound tasks (thinking)
Image processing, ML prep, heavy math
⚙️ 2. The Basic Pattern (Threads)
Method
What It Does
Returns
submit(fn, *args)
Schedules function to run in background
Future object
future.result()
Waits for and returns the result
Function's return value
shutdown()
Releases worker threads
None
⚡ 3. Running Many Tasks at Once
Approach
When to Use
submit()
Different functions, custom handling
Individual Futures
map()
Same function, many inputs
Iterator of results (in order)
4. CPU Parallelism with ProcessPoolExecutor
Memory
GIL
CPU Usage
Shared
One GIL (blocks CPU work)
1 core effective
Separate
Each has own GIL
All cores!
🧩 5. Futures — Understanding the Object
A Future represents a pending operation. It can be in different states:
State
Meaning
Check With
Running
Task is currently being executed
future.running()
Done
Task completed (success or error)
future.done()
Cancelled
Task was cancelled before running
future.cancelled()
🌀 6. Handling Exceptions in Parallel Tasks
If a function raises an exception inside a worker, .result() will re-raise it in the main thread:
On Success
On Error
Returns the value
Raises the exception
future.exception()
Returns None
Returns the exception object
7. Real-World Example — Parallel Web Requests
Time for 100 URLs (0.3s each)
Speedup
Sequential (one by one)
30 seconds
1x (baseline)
ThreadPool (10 workers)
~3 seconds
~10x faster!
- API pipelines
- ETL ingestion
⚡ 8. Real-World Example — CPU Parallel Data Processing
- AI data prep
- big dataset cleaning
- batch computation services
🔄 9. Mixing Concurrency & Parallelism
- I/O parallelism (threads)
- CPU parallelism (processes)
- Task scheduling (asyncio)
Pipeline Stage
Best Tool
Why
Download data
I/O-bound: waiting for servers
Parse/transform data
CPU-bound: heavy computation
Coordinate/schedule
asyncio
Lightweight: manage task flow
- asyncio → orchestrates
- ThreadPoolExecutor → handles blocking file/network
- ProcessPoolExecutor → handles heavy CPU tasks
This is how modern Python backends (FastAPI, aiohttp) achieve massive throughput.
📦 10. Choosing the Right Executor
Scenario
Best Choice
Many API calls
Downloading files
Reading thousands of files
Image processing
ML preprocessing
Large math loops
ETL pipelines
Both (mixed)
🧩 11-20. Behind the Scenes & Advanced Patterns
Task queue, worker threads/processes, IPC mechanisms
Pickling costs, lambda limitations, using top-level functions
Grouping tasks for efficiency, reducing pickling overhead
Optimizing executor.map() with chunksize parameter
multiprocessing.Manager, Queue, shared_memory
Pinning threads to CPU cores for consistent latency
AsyncIO → ThreadPool → ProcessPool → AsyncIO pattern
🧪 21. Full Real-World Example — Data ETL Pipeline
This is TRUE professional pipeline architecture.
🧬 22. Zero-Copy Shared Memory (Python 3.8+)
Normal multiprocessing copies all data via pickle.
100MB Array to 4 Workers
Memory Used
Normal (pickle)
~2 seconds
400MB (4 copies)
Shared memory
~0.001 seconds
100MB (1 shared)
- ❌ Large NumPy arrays (100MB+) = slow
- ❌ ML tensors = slow
- ❌ Video frames = slow
- PyTorch multiprocessing
- TensorFlow data pipelines
- High-performance ETL
🚀 23-32. Expert-Level Parallel Patterns
Split work, collect results — backbone of scalable systems
Separate pools for IO, CPU, and orchestration
Parallel map, sequential reduce — Hadoop/Spark ancestor
Task graph, scheduler, future tracking — Ray/Dask concepts
ProcessPool + sockets for multi-machine tasks
AsyncIO orchestration + ThreadPool I/O + ProcessPool CPU
🚀 32. Master Hybrid Pipeline (Complete Example)
The ultimate architecture combining AsyncIO + Threads + Processes:
- ✔ Netflix data pipelines
- ✔ TikTok's ML recommendation ingest
- ✔ YouTube's batch processing
- ✔ OpenAI's internal preprocessors
🎉 Conclusion
You now understand ULTRA-ADVANCED concurrency and parallelism:
This module is the foundation of high-performance Python systems — from ML pipelines to scalable backend services.
📋 Quick Reference — Parallelism
Syntax
What it does
ThreadPoolExecutor(max_workers=4)
Pool for I/O-bound tasks
ProcessPoolExecutor(max_workers=4)
Pool for CPU-bound tasks
executor.submit(fn, arg)
Submit one task, returns Future
executor.map(fn, items)
Map function over iterable
as_completed(futures)
Iterate futures as they finish
You can now use ThreadPoolExecutor and ProcessPoolExecutor to parallelize real work efficiently and safely.
Up next: Profiling — learn to measure and optimise Python performance like a senior engineer.
Practice quiz
Which concurrent.futures executor is best for I/O-bound work like API calls and downloads?
- ProcessPoolExecutor
- asyncio.Executor
- ThreadPoolExecutor
- Both are equally bad for I/O
Answer: ThreadPoolExecutor. ThreadPoolExecutor suits I/O-bound tasks where workers spend most time waiting.
Which executor is best for CPU-bound work like image processing or heavy math?
- ProcessPoolExecutor
- ThreadPoolExecutor
- A single thread
- asyncio alone
Answer: ProcessPoolExecutor. ProcessPoolExecutor uses separate processes, each with its own GIL, to use multiple cores for CPU work.
What does executor.submit(fn, arg) return?
- The function's result immediately
- None
- A list of results
- A Future object representing the pending work
Answer: A Future object representing the pending work. submit() schedules the call and immediately returns a Future you can query later.
What does calling future.result() do?
- Cancels the task
- Blocks until the task finishes, then returns its value (or re-raises its exception)
- Returns instantly even if not done
- Starts the task
Answer: Blocks until the task finishes, then returns its value (or re-raises its exception). .result() waits for completion and returns the value, or re-raises any exception from the worker.
What ordering does executor.map(fn, items) guarantee for its results?
- Results in the same order as the input items
- Results in completion order (fastest first)
- Random order
- Reverse order
Answer: Results in the same order as the input items. map() preserves input order regardless of which task finishes first.
If a function raises an exception inside a worker, when is it surfaced?
- Immediately in the worker, crashing the program
- It is silently ignored
- When you call future.result(), which re-raises it in the calling thread
- Only when the executor shuts down
Answer: When you call future.result(), which re-raises it in the calling thread. The exception is stored in the Future and re-raised when you call .result().
What is a Future?
- A scheduled date for the task
- An object representing work that has started but may not be finished yet
- A type of thread
- The result value itself
Answer: An object representing work that has started but may not be finished yet. A Future is a placeholder for a pending result that you can check, wait on, or cancel.
Why can't ThreadPoolExecutor speed up pure-Python CPU-bound loops much?
- Threads are slower than processes always
- Threads can't share memory
- ThreadPoolExecutor has a 1-worker limit
- The GIL lets only one thread run Python bytecode at a time
Answer: The GIL lets only one thread run Python bytecode at a time. Because of the GIL, threads can't execute Python bytecode truly in parallel, so CPU-bound code stays on one core.
Using a ProcessPoolExecutor as 'with ... as executor:' provides what benefit?
- It disables the GIL
- Automatic cleanup/shutdown of workers when the block exits
- It makes tasks run sequentially
- It removes the need for results
Answer: Automatic cleanup/shutdown of workers when the block exits. The context manager automatically shuts down the executor, so you don't call shutdown() manually.
Which method returns the stored exception (or None) without raising it?
- future.result()
- future.running()
- future.exception()
- future.cancel()
Answer: future.exception(). future.exception() returns the exception object if the task failed, or None on success.