Advanced Python 4— GIL in Python
If you work with threads in Python, you will often hear about the GIL, or Global Interpreter Lock.
What is GIL?
The GIL allows only one thread to execute Python code at a time, even on a multi-core CPU.
So even if your program has multiple threads, only one thread runs Python instructions at any moment.
It is a mechanism in CPython (the standard Python interpreter).
✔ Only ONE thread executes Python bytecode at a time
✔ Even on multi-core CPUs
Why Does Python Have a GIL?
The GIL exists to:
The GIL makes sure Python objects are not deleted while another thread is still using them, keeping memory management safe.
The GIL is a trade-off: it limits CPU-bound threading but keeps Python simple, safe, and fast for most programs.
Example:
import threading
def count():
x = 0
for _ in range(100_000_000):
x += 1
t1 = threading.Thread(target=count)
t2 = threading.Thread(target=count)
t1.start()
t2.start()
t1.join()
t2.join()What you might expect
- Two threads
- Two CPU cores
- Program runs twice as fast
What actually happens
- Due to the GIL, only one thread runs at a time
- Threads take turns using the CPU
- No real speed improvement
Threads do not help for CPU-heavy work in Python
Threads are concurrent, not parallel (for Python code).
When Threads Still Help (I/O-bound Tasks)
The GIL is released when a thread is waiting for I/O.
Example:
- Reading a file
- Waiting for a network response
- Calling an API
Threads are useful for I/O-bound tasks
How to Bypass the GIL
Using native (C/C++) extensions.
Why?
✔ Extensions can release GIL manually
✔ Allow true parallel execution
Many high-performance libraries:
Get Abhishek Jain’s stories in your inbox
Join Medium for free to get updates from this writer.
✔ NumPy
✔ PyTorch
✔ TensorFlow
Why fast?
➡️Heavy work done in C/C++
➡️GIL released during computation
Even though you write Python code, many libraries are actually:
✔ Implemented in C / C++ / CUDA
✔ Python is just a wrapper / interface
Your Python code acts like:
➡️ A remote control
➡️ Calling fast native code underneath
When we call a NumPy function from Python, the function we invoke is typically implemented in optimized C (or C++) code underneath. The Python layer acts as a wrapper that forwards the request to NumPy’s native backend through a well-defined API. The actual computation is then performed in compiled native code (Compiled native code means code that has been translated directly into machine instructions that the CPU can execute), which runs much faster than Python bytecode. During this execution, Python’s interpreter overhead is avoided (Python interpreter overhead is largely avoided during the heavy computation, because the actual work is executed by compiled native C/C++ code.) and in many cases the GIL is released. Once the computation finishes, the result is passed back through the API to the Python layer. Python then presents the final output to the user as a normal Python object. This design is the reason NumPy operations are significantly faster than equivalent logic written using pure Python loops.
Why GIL is not a problem here
Because:
✔ Native code runs outside Python interpreter
✔ Libraries release GIL
✔ Multiple threads can run native code
Use multiprocessing
Each process has:
- Its own Python interpreter
- Its own GIL
from multiprocessing import Process
def count():
x = 0
for _ in range(100_000_000):
x += 1
p1 = Process(target=count)
p2 = Process(target=count)
p1.start()
p2.start()
p1.join()
p2.join()Now:
- Processes run in parallel
- Multiple CPU cores are used
Common Misunderstandings
❌ “Python cannot use multiple cores”
✅ Python can, using processes
❌ “Threads are useless in Python”
✅ Threads are great for I/O-bound tasks
Rule of thumb:
- Use threads for I/O-bound work
- Use processes for CPU-bound work
Why C/C++ doesn’t have GIL?
C and C++ don’t need a GIL because they don’t manage memory automatically or run code inside a shared interpreter — thread safety is left to the programmer.









