The GIL, threads and processes

Concurrency in Python services Mid-level Python 3.13+ PostgreSQL 17+ Reviewed By Best Latest Apps

Pick the fix

A request handler resizes images in pure Python and takes 800 ms of CPU. Throughput is poor under a thread pool. Best change?

Options
Hint 1 of 1

Is the work waiting or computing?

Choose an answer, then open Show answer below to check it.

Show answer

Answer: A Run the resize in a process pool or a background worker, or use a library that releases the GIL

Correct. CPU work needs real parallelism outside one interpreter lock.

In standard CPython only one thread runs Python bytecode at a time, so CPU-bound threads take turns. Separate processes each have their own interpreter and GIL and can use several cores.

Why not the others?
  • B Increase the thread pool to 64. More threads still share one GIL for Python bytecode.
  • C Convert the handler to async. async doesn't make CPU work parallel.
  • D Add caching headers. Doesn't change the CPU cost of new images.

30-second answer

The global interpreter lock lets only one thread run Python bytecode at a time in standard CPython. Threads still help for I/O-bound work, because the lock is released while waiting on sockets and files. CPU-bound Python code doesn't get faster with threads; use processes (multiprocessing, ProcessPoolExecutor), native extensions that release the GIL, or move the work out of the request path. Python 3.13 added an experimental free-threaded build without the GIL; 3.14 made it officially supported (PEP 779), but still as a separate, optional build.

Practise saying it · 35 s. No recording, just a timer

Threads or processes in Python?

Say your answer out loud, then check which of these points you covered:

  • GIL: one thread runs bytecode
  • Threads for I/O
  • Processes for CPU
  • C extensions may release GIL
  • Free-threaded: optional build, supported from 3.14

Deep dive

NumPy, hashing and compression often release the GIL inside C code, so threads can help.

Processes don't share memory; arguments and results are pickled.

Web servers run several worker processes per machine for CPU parallelism (gunicorn -w, uvicorn workers).

The free-threaded build (python3.13t or python3.14t) needs extensions marked as supporting it; importing one that isn't turns the GIL back on, with a warning.

Fan CPU work out to processes Ran on Python 3.13.5 · mypy --strict
Python 3.13
from concurrent.futures import ProcessPoolExecutor


def work(n: int) -> int:
    return sum(i * i for i in range(n))


if __name__ == "__main__":
    with ProcessPoolExecutor(max_workers=2) as pool:
        print(list(pool.map(work, [10, 100, 1000])))

Common mistakes

  • Using threads to speed up a CPU-heavy loop.
  • Assuming the GIL makes your code thread-safe (compound operations can still interleave).
  • Spawning a process per request.

Senior take & follow-ups

I/O-bound → threads or asyncio; CPU-bound → processes; mention the free-threaded build accurately: experimental in 3.13, supported but optional from 3.14.

Follow-up questions

Think of your answer first, then open the hint.

  1. Does the GIL prevent race conditions?

    Show hint

    No; it protects interpreter internals, not your read-modify-write sequences.

  2. What changed in 3.13 and 3.14?

    Show hint

    3.13 added an experimental free-threaded build (PEP 703); 3.14 made it officially supported but still optional (PEP 779).

Similar on Android: Coroutines vs threads Kotlin threads run truly in parallel on the JVM; CPython threads share one interpreter lock.

Suggest a fix if something here is wrong or out of date.