Multiprocessing
The threading module lets several threads take turns inside a single Python interpreter, but CPython's Global Interpreter Lock (GIL) means only one of those threads ever executes Python bytecode at a given instant — so threading cannot make CPU-heavy work run faster. The multiprocessing module takes a different approach entirely: instead of multiple threads inside one interpreter, it starts multiple full operating-system processes, each running its own independent Python interpreter with its own GIL and its own memory space. That gives you genuine parallelism across CPU cores.
Creating a Process
import multiprocessing as mp
def square(n):
print(f"square({n}) = {n * n}")
if __name__ == "__main__":
p1 = mp.Process(target=square, args=(4,))
p2 = mp.Process(target=square, args=(7,))
p1.start()
p2.start()
p1.join()
p2.join()
print("Both processes finished")square(4) = 16 square(7) = 49 Both processes finished
The API deliberately mirrors threading.Thread: .start() launches it, .join() waits for it to finish. The key difference is underneath — p1 and p2 are entirely separate processes, each with its own copy of the Python interpreter and its own memory, so they can genuinely execute Python code simultaneously on separate CPU cores.
Pool: parallel map over a list of work
Creating individual Process objects by hand is fine for a couple of tasks, but for "run this function over a big list of inputs, spread across my CPU cores," multiprocessing.Pool is much more convenient.
import multiprocessing as mp
def is_prime(n):
if n < 2:
return False
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
return False
return True
if __name__ == "__main__":
numbers = list(range(100_000, 100_050))
with mp.Pool(processes=4) as pool:
results = pool.map(is_prime, numbers)
primes = [n for n, prime in zip(numbers, results) if prime]
print(primes)[100003, 100019, 100043, 100049]
pool.map(is_prime, numbers) splits numbers across the pool's worker processes, runs is_prime on each chunk in parallel, and returns the results in the original order — the same mental model as the built-in map(), just spread across multiple CPU cores.
When multiprocessing wins
Checking whether a number is prime is CPU-bound: the process is never waiting on the network or disk, it is purely doing computation. That is exactly the kind of workload where multiprocessing pays off and threading would not, because the GIL would keep all the actual computation on a single core no matter how many threads you created.
The cost of processes
Threading vs. multiprocessing
threading | multiprocessing | |
|---|---|---|
Memory usage | Low — threads share one process’s memory | Higher — each process gets its own interpreter and memory space |
Impact of the GIL | Limits to one thread executing Python bytecode at a time | Bypassed entirely — each process has its own GIL |
Best use case | I/O-bound work: network calls, file/database I/O, waiting | CPU-bound work: number crunching, data processing, image/video work |
Starting a process is noticeably slower than starting a thread — reuse a
Poolacross many small tasks instead of creating new processes for each one.Data passed into and returned from worker processes must be picklable — plain functions, numbers, strings, lists, and dicts are fine; things like open file handles or database connections generally are not.
If your bottleneck is waiting on the network or disk, reach for
threading(orasyncio) first — multiprocessing’s overhead buys you nothing there.