Are locks unnecessary in multi-threaded Python code because of the GIL?
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
The GIL in CPython does not eliminate the need for locks. It only ensures that one thread executes Python bytecode at a time inside the interpreter. If multiple threads access shared mutable state, you still need synchronization whenever correctness depends on a multi-step operation happening atomically.
What the GIL Actually Protects
The GIL is an interpreter-level lock, not an application-level correctness guarantee. It prevents simultaneous execution of Python bytecode in multiple threads, but threads can still interleave between bytecode instructions. That means a logical operation such as incrementing a counter can be interrupted halfway through.
A high-level line like counter += 1 is not one indivisible application event. It involves reading, computing, and writing.
Race Condition Example
This code looks simple, but it can still produce the wrong result under threading.
The GIL does not guarantee that the final value will be exactly 400000. The problem is a race on shared state, not memory corruption in the C sense.
Use threading.Lock for Shared Mutable State
When multiple threads update the same value or container, protect the critical section with a lock.
This serializes the update so the shared invariant stays correct.
Some Built-Ins Are Not a Complete Safety Story
Developers sometimes hear that operations on lists or dicts are "thread-safe" in CPython. That statement is easy to misuse. Some individual operations may be implemented safely at the interpreter level, but compound logic is still vulnerable.
For example, this pattern is unsafe without a lock:
- check whether a key exists
- compute a new value
- update the dictionary
Another thread can modify the dictionary between those steps.
Use Higher-Level Thread-Safe Patterns When Possible
Locks are important, but sometimes the best solution is to avoid shared mutable state altogether. A queue is often better than a shared list for producer-consumer work.
queue.Queue removes much of the manual locking burden and makes ownership clearer.
CPU-Bound Threads Still Do Not Scale Well
The GIL also matters for performance. For I O-bound work, threads are often useful because blocking calls can release the GIL. For CPU-bound pure Python work, threads do not give true parallel speedup on multiple cores in CPython.
For CPU-bound tasks, prefer multiprocessing, native extensions, or vectorized libraries that release the GIL internally.
Common Pitfalls
- Assuming the GIL makes all shared-state access automatically safe.
- Treating
counter += 1as one atomic application action. - Trusting built-in container operations without checking whether the whole workflow is multi-step.
- Using threads for CPU-bound speedups in pure Python.
- Reaching for locks everywhere when a queue or ownership redesign would be simpler.
Summary
- The GIL is not a replacement for locks.
- Shared mutable state still needs synchronization when multiple threads can interleave.
- Use
threading.Lockfor critical sections that must be atomic. - Prefer thread-safe patterns such as queues when they fit the problem.
- Use threads mainly for I O-bound work in CPython, not for CPU-bound parallelism.

