Concurrency vs Parallelism in Python: What Really Matters
Concurrency and parallelism are often confused, but they solve different problems. Understanding the distinction helps you choose the right model in Python: async, threads, or processes.
Many performance issues in Python are not caused by slow code. They are caused by choosing the wrong execution model.
Concurrency and parallelism are related, but they are not the same. When you understand the difference, you can make better decisions about asyncio, threads, processes, and overall system design.
The Core Idea
Concurrency is about handling multiple tasks by making progress on more than one task at a time.
Parallelism is about running multiple tasks at the same time.
A concurrent system can interleave work and remain responsive even if tasks are not executing simultaneously. A parallel system increases throughput by executing tasks simultaneously, typically on multiple CPU cores.
A Simple Mental Model
If you have a web service receiving requests:
- Concurrency helps you handle many requests without blocking on slow I/O such as network or database calls.
- Parallelism helps you speed up CPU-heavy work such as image processing or large computations.
A common mistake is trying to use concurrency tools to speed up CPU work, or using parallelism tools when the real bottleneck is I/O.
Concurrency in Python
Concurrency in Python is often used to improve responsiveness and utilization during waiting time.
Typical concurrency approaches include:
- asyncio for cooperative scheduling of I/O-bound tasks
- threading for I/O-bound tasks that call blocking libraries
- event loops and non-blocking sockets
Concurrency with asyncio
import asyncio
async def fetch_data(i: int) -> str:
await asyncio.sleep(1)
return f"data-{i}"
async def main():
tasks = [fetch_data(i) for i in range(3)]
results = await asyncio.gather(*tasks)
print(results)
asyncio.run(main())
This is concurrency. Tasks make progress while others are waiting.
It is a strong fit for I/O-bound workloads such as HTTP calls, database queries, message queues, and file operations when you have async-compatible libraries.
Threads as a practical concurrency tool
from threading import Thread
import time
def io_task(i: int) -> None:
time.sleep(1)
print(f"done-{i}")
threads = [Thread(target=io_task, args=(i,)) for i in range(3)]
for t in threads:
t.start()
for t in threads:
t.join()
Even in CPython, threads can help if your workload spends time waiting on I/O.
Parallelism in Python
Parallelism is about doing more CPU work in less wall-clock time by running tasks simultaneously.
In CPython, true CPU parallelism typically uses multiple processes, not threads.
Parallelism with multiprocessing
from multiprocessing import Pool
def compute(x: int) -> int:
return x * x
if __name__ == "__main__":
with Pool(4) as p:
result = p.map(compute, range(10))
print(result)
Each process has its own interpreter and its own GIL, allowing multiple CPU cores to be used at the same time.
This is a better fit for CPU-heavy workloads such as batch transformations, parsing, compression, feature engineering, and numerical work.
The GIL and Why It Matters
In CPython, the Global Interpreter Lock allows only one thread to execute Python bytecode at a time within a single process.
This has practical consequences:
- Threads do not speed up CPU-bound Python code in most cases.
- Threads are effective for I/O-bound workloads.
- Processes enable true parallelism for CPU-bound tasks.
A useful rule is simple: if the task is waiting, use concurrency. If the task is computing, use parallelism.
A Practical Example from Real Systems
In real-world systems, I have used both models depending on the bottleneck.
When integrating multiple external APIs where network latency dominated execution time, using concurrency with asyncio significantly improved throughput without increasing CPU usage.
In data processing pipelines where parsing and heavy transformations were CPU-bound, switching to multiprocessing reduced total execution time by distributing work across available cores.
The key insight was not choosing a tool first, but identifying what was actually slow.
Choosing the Right Model
Before changing your architecture, ask:
- Is the workload mostly waiting or computing?
- Is the slow part network, disk, database, or CPU?
- Are the libraries async-friendly or blocking?
- Is the workload large enough to justify process overhead?
Use concurrency when:
- The workload is I/O-bound.
- You need responsiveness under waiting time.
- You want to avoid blocking the main execution flow.
Use parallelism when:
- The workload is CPU-bound.
- You want to utilize multiple CPU cores.
- You are running heavy computations or batch jobs.
Final Take
Concurrency and parallelism are complementary tools.
Concurrency helps you remain efficient while waiting.
Parallelism helps you compute faster by using multiple cores.
The real skill is not knowing the APIs. It is knowing which model matches your bottleneck.