parallelization
Python
loops
multiprocessing
performance optimization

How do I parallelize a simple Python loop?

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

Introduction

Parallelizing a Python loop can cut runtime dramatically, but only if you choose the right model for the workload. The main decision is whether your loop is CPU bound or I/O bound. CPU bound loops need multiple processes because of the Global Interpreter Lock (GIL), while I/O bound loops often benefit from threads or async I/O. Another important point is task size: if each loop iteration is tiny, overhead from process startup and data transfer can remove any speedup. This guide gives a practical workflow for converting a basic loop to parallel execution and validating that it actually improves performance.

Core Sections

Start with a serial baseline

Before parallelizing, measure the current loop so you can verify gains.

python
1import time
2
3def work(x: int) -> int:
4    total = 0
5    for i in range(200_000):
6        total += (x * i) % 97
7    return total
8
9inputs = list(range(200))
10
11start = time.perf_counter()
12results = [work(x) for x in inputs]
13print(f"serial: {time.perf_counter() - start:.2f}s")

This baseline lets you compare speed and correctness after changes.

Use ProcessPoolExecutor for CPU bound work

For pure computation, processes are usually the correct default.

python
1from concurrent.futures import ProcessPoolExecutor
2import time
3
4def parallel_compute(inputs):
5    with ProcessPoolExecutor() as pool:
6        return list(pool.map(work, inputs, chunksize=10))
7
8start = time.perf_counter()
9results_parallel = parallel_compute(inputs)
10print(f"parallel: {time.perf_counter() - start:.2f}s")

Tips:

  • Put worker functions at module top level so they are pickleable.
  • Guard entry with if __name__ == "__main__": on Windows and macOS spawn mode.
  • Tune chunksize for large iterables to reduce scheduling overhead.

Use threads for I/O bound loops

If iterations mostly wait on network or disk, thread pools are simpler and effective.

python
1from concurrent.futures import ThreadPoolExecutor
2import requests
3
4def fetch(url: str) -> int:
5    return len(requests.get(url, timeout=5).text)
6
7urls = ["https://example.com"] * 50
8
9with ThreadPoolExecutor(max_workers=20) as pool:
10    sizes = list(pool.map(fetch, urls))

Threads can overlap waiting time even though CPU heavy Python bytecode still contends on the GIL.

Validate correctness and stability

Parallel code can reorder results or surface hidden exceptions. Always verify output parity with the serial version and add deterministic tests.

python
assert results == results_parallel

For large jobs, collect failures explicitly and retry transient errors instead of silently skipping them.

Common Pitfalls

  • Parallelizing very small tasks where process and serialization overhead are larger than the actual work.
  • Choosing threads for CPU heavy loops and expecting linear speedups despite the GIL.
  • Passing huge mutable objects to workers each iteration, causing expensive pickling and memory pressure.
  • Ignoring error handling in futures, which can hide failed tasks until much later.
  • Benchmarking only once instead of measuring multiple runs and comparing median runtime.

Production Readiness Check

Before closing the task, run a short validation loop on representative inputs and one intentional failure case. Confirm that your code path behaves correctly for normal data, empty data, and malformed data. Capture at least one measurable signal such as runtime, memory use, or error rate, then compare it to your baseline so regressions are visible. Keep this check lightweight so it can run in local development and CI without slowing feedback too much. A simple checklist plus one executable smoke test prevents most regressions after refactors and library upgrades.

text
11. Run happy-path example
22. Run edge-case example
33. Run failure-path example
44. Capture one performance or reliability metric
55. Verify output format and error handling

Summary

The best way to parallelize a simple Python loop is to classify the workload first, then choose the executor model that matches it. Use ProcessPoolExecutor for CPU bound tasks, ThreadPoolExecutor for I/O bound tasks, and keep task granularity large enough to amortize overhead. Preserve a serial baseline, validate results, and benchmark repeatedly. Parallelism is not a free win, but with the right model and measurements, it is often one of the highest impact optimizations you can make in Python.


Related reading
Course
Intermediate
27 lessons
15 hours
DSA Fundamentals

Master algorithmic patterns and data structures through hands-on LeetCode-style problems - from arrays and hashing to dynamic programming and advanced graphs.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

All Rights Reserved.