what is GIL in python

What is GIL in Python and How it works (With Examples)

If you have tried to speed up Python with threads and seen no performance gain, you have probably run into the Global Interpreter Lock, or GIL. Understanding what the GIL is in Python, how it works, and when it matters will help you choose the right tools for your Python projects.

Quick Answer: What the GIL Is and Why It Exists

One‑sentence definition of the GIL

The Global Interpreter Lock (GIL) in Python is a single global mutex (lock) in the CPython interpreter that allows only one thread at a time to execute Python bytecode.

Simple analogy: one person with the microphone

Imagine a conference room with many people (threads) who all want to speak.

  • There is only one microphone (the GIL).
  • Only the person holding the microphone can speak to the room (run Python code).
  • Others may be waiting, or they may be busy doing things outside the microphone (I/O, C code).

This means your programme can have many threads, but only one of them can run Python code at any instant in CPython.

Why CPython uses a GIL (memory safety and simplicity)

The GIL is not there to annoy you. It exists mainly for:

  • Memory safety: CPython uses reference counting to manage memory. The GIL protects this shared state so that multiple threads do not corrupt memory when updating reference counts.
  • Simpler implementation: With a single global lock, the interpreter code is much simpler. There is less risk of complex deadlocks and subtle bugs from many small locks.
  • Performance for single‑threaded programmes: For many common, single‑threaded use cases, the GIL keeps CPython fast enough without a lot of extra synchronisation overhead.

So the GIL is a trade‑off: it makes the interpreter simpler and safer, but it also limits parallel execution of Python code across CPU cores.

How the GIL Works in CPython

CPython vs “Python the language”

The GIL is not part of the Python language itself. It is an implementation detail of:

  • CPython – the main, official Python implementation that most people use.

Other implementations can behave differently, for example:

  • PyPy – a different Python interpreter with its own JIT compiler, also historically with a GIL.
  • Jython – Python on the JVM (Java Virtual Machine), uses Java threads and does not use the CPython GIL.
  • IronPython – Python on .NET, also without the CPython GIL.
  • Ruby MRI – not Python, but the main Ruby interpreter, also uses a global interpreter lock like CPython.

When people say “Python has a GIL”, they almost always mean “CPython has a GIL”.

The lock around Python object access

The key behaviour of the GIL is simple:

  • Only one thread at a time can hold the GIL.
  • Only the thread holding the GIL can execute Python bytecode (normal Python code).

This means that, in a multi‑threaded CPython programme:

  • Threads may run on different OS threads internally.
  • But at any instant, only one of them is actually running Python code.

Other threads may be:

  • Waiting to get the GIL, or
  • Running C code that temporarily released the GIL, or
  • Blocked on I/O, sleep, or other system calls.

When the GIL is acquired and released

From your point of view as a developer, you can think of it like this:

  • When a thread starts running Python code, it acquires the GIL.
  • When it does certain operations, the GIL is released so other threads can run.

Some important moments when the GIL is released:

  • Blocking I/O operations
    Examples: reading from a file, waiting on a socket, network requests, database calls.
    The interpreter releases the GIL while the OS is waiting for data. Another thread can then acquire the GIL and run Python code.
  • sleep() and other blocking system calls
    When you call time.sleep(), the GIL is released while the thread is sleeping.
  • C extensions that explicitly release the GIL
    Many libraries written in C (for example parts of NumPy, SciPy, OpenCV, and some ML libraries) release the GIL around heavy computation so that other Python threads can run in parallel.

The interpreter also periodically switches between threads even when they are all CPU‑bound. It does this by checking “check intervals” or “switch intervals” so one thread does not monopolise the GIL forever. However, this still does not give true multi‑core parallelism for pure Python code, because the GIL still allows only one executing thread at a time.

GIL and reference counting (basic idea)

CPython tracks how many references each object has. This is called reference counting. When an object’s reference count falls to zero, CPython knows it can free that memory.

Without the GIL, two threads could try to update the same reference count at the same time and corrupt memory. To avoid this, CPython protects reference count updates by holding the GIL when running Python code. This is a big reason the GIL exists.

Important detail: the GIL protects the interpreter’s internals, but it does not magically make your own data structures thread‑safe. You may still need locks around your own shared state.

How the GIL Affects Multithreading and Performance

CPU‑bound vs I/O‑bound work

To understand how the GIL affects performance, you must separate two types of workloads:

  • CPU‑bound: Most time is spent doing calculations on the CPU (e.g. numerical loops, image processing in pure Python, heavy data transformations).
  • I/O‑bound: Most time is spent waiting for external resources (e.g. network, disk, API calls, web scraping).

With the GIL in CPython:

  • CPU‑bound threads do not scale across cores
    If you start several threads, all doing heavy Python computation, they will take turns holding the GIL. So they will run mostly one after another, not in true parallel on multiple cores.
  • I/O‑bound threads can still be very useful
    While one thread is waiting on I/O and has released the GIL, another thread can run Python code. This gives you higher throughput for I/O‑heavy tasks like web scraping, API clients, or file operations.

Common performance surprises

“My threaded loop is slower than a single loop”

This is a very common problem. Example:

import threading

def count(n):
    while n > 0:
        n -= 1

N = 50_000_000

# Single thread
count(N)

# Multi‑threaded
t1 = threading.Thread(target=count, args=(N//2,))
t2 = threading.Thread(target=count, args=(N//2,))
t1.start(); t2.start()
t1.join(); t2.join()

You might expect the multi‑threaded version to be about twice as fast on a two‑core machine. With the GIL, it is often the same speed or slower because:

  • Only one thread can run Python bytecode at once.
  • The interpreter has extra overhead from context switching (passing the GIL back and forth between threads).

Thread context switching overhead

Every time the interpreter switches which thread holds the GIL, there is a small overhead. For CPU‑bound work, this overhead gives you extra cost for no extra speed.

Important: the GIL does not guarantee that Python operations are atomic (indivisible). For example, the expression x += 1 is not guaranteed to be atomic between threads; it involves multiple bytecode instructions. You may still have race conditions and need your own locks for shared variables.

Real‑world examples

CPU‑bound: data processing loop

Suppose you write a loop that computes some statistics over a huge list using pure Python. Adding more threads will usually not help, because they will just compete for the GIL.

Better options:

  • Use multiprocessing to use multiple processes on multiple cores.
  • Move heavy parts into NumPy, Pandas, or Cython so the heavy work happens in C and can release the GIL.

I/O‑bound: web scraping or API calls

Now consider a script that fetches thousands of web pages:

  • Most time is spent waiting for remote servers.
  • During that wait, the GIL is released.

Here, threads can help a lot. Each thread can start a request, release the GIL while waiting, and another thread can run meanwhile. Alternatively, you can use asyncio for even lighter‑weight concurrency.

When You Should Care About the GIL (and When You Can Ignore It)

Situations where the GIL is usually not a problem

  • Simple scripts and CLI tools
    Small command‑line utilities, automation scripts, and single‑threaded programmes are rarely affected.
  • Web applications that are mostly I/O‑bound
    Typical web apps spend time waiting on databases, caches, and networks. The web server process model (e.g. multiple worker processes) and I/O waits mean the GIL is usually not the main bottleneck.
  • Many data science workflows
    NumPy, Pandas, SciPy, and similar libraries are written in C and often release the GIL while doing heavy number crunching. This means they can use multiple cores internally, even from a single Python process.

Situations where the GIL is likely a problem

  • Heavy CPU‑bound tasks written in pure Python
    For example, large numerical loops, simulations, or encoding/decoding algorithms implemented in Python.
  • Parallel algorithms that should use many cores
    If you design an algorithm that you expect to run in parallel across N cores using threads, CPython’s GIL will prevent that for pure Python code.

In these cases, you should think early about using multiprocessing, C‑backed libraries, or other runtimes.

Practical Ways to Work Around GIL Limitations

Use multiprocessing for CPU‑bound tasks

The multiprocessing module creates separate OS processes. Each process has:

  • Its own Python interpreter.
  • Its own GIL.

Because processes do not share the same interpreter, they can run truly in parallel on different CPU cores. This is the most common way to “bypass” the GIL for CPU‑bound tasks.

Simple example with multiprocessing.Pool

from multiprocessing import Pool
import math

def work(x):
    # Some CPU‑heavy calculation
    return math.sqrt(x) ** 3

if __name__ == "__main__":
    data = list(range(10_000_000))

    with Pool() as pool:
        results = pool.map(work, data)

Here, the data is split across worker processes. They all run in parallel on different cores, and the GIL does not block them from executing at the same time.

Trade‑offs:

  • Data must be serialised (pickled) between processes, which has overhead.
  • Shared state is harder; you need queues, pipes, shared memory, or managers.

Use C‑backed libraries that release the GIL

Many scientific and high‑performance libraries are written in C, C++, or Fortran and provide Python bindings. Examples include:

  • NumPy
  • Pandas
  • SciPy
  • OpenCV (cv2)
  • Various machine learning libraries

These libraries often:

  • Do heavy computation in compiled code.
  • Release the GIL while they are working.

This allows:

  • True parallelism inside the library (e.g. using OpenMP, BLAS, or internal threads).
  • Other Python threads to do useful work while one thread is inside a C extension.

A common best practice is: keep heavy loops out of pure Python and inside C‑backed libraries whenever possible.

Use asyncio or threads for I/O‑bound concurrency

Threads for blocking I/O

For programmes that make many network requests, web scraping, or talk to many slow APIs, threads can work very well even with the GIL. While one thread waits for the network, others can run.

asyncio for scalable I/O

asyncio provides asynchronous I/O using a single thread and an event loop. It is very efficient when you have:

  • Many connections.
  • Mostly I/O‑bound work.

The GIL has little effect on asyncio because there is only one main thread executing Python code. The limiting factor is network speed, not the GIL.

Answering a common question: Does the GIL affect asyncio?

  • Not in the way it affects multi‑threaded CPU‑bound code.
  • asyncio is still single‑threaded Python, so there is no attempt at multi‑core parallelism.
  • The main benefit of asyncio is high concurrency for I/O, not CPU parallelism.

Alternative Python implementations and no‑GIL builds

There are different Python interpreters and new work to remove the GIL:

  • Jython, IronPython – these use their host platform’s threading model and do not have the CPython GIL, but they may not support all modern Python features or libraries.
  • PyPy – focuses on speed with a JIT; historically also uses a GIL.
  • Python 3.13 no‑GIL build – work based on PEP 703 introduces a build of CPython without the GIL, using a different memory allocation strategy (including mimalloc) and fine‑grained locking.

Does Python 3.13 remove the GIL?

  • As of Python 3.13, there is an optional no‑GIL build. The traditional GIL‑based build still exists.
  • Adopting no‑GIL in production will take time as libraries and tools adapt.

For most teams today, the practical approach is still to design with the current CPython GIL in mind, while keeping an eye on the no‑GIL roadmap.

Common Myths and Misunderstandings About the GIL

“Python cannot use multiple cores”

This is misleading. More accurate statements are:

  • A single CPython process with CPU‑bound threads cannot fully use multiple cores with pure Python code.
  • Python programmes as a whole can use multiple cores using:
    • Multiple processes (e.g. multiprocessing, web server workers).
    • C‑extensions that release the GIL and run in parallel.

“Threads are useless in Python”

Threads are not useless. They are very useful for:

  • I/O‑bound tasks (network, disk, user input).
  • Background tasks such as logging or monitoring.
  • Interacting with C libraries that release the GIL.

They are just not effective for CPU‑bound parallelism in pure Python.

“The GIL is only bad”

The GIL has real downsides, but it also has benefits:

  • Simpler interpreter code.
  • Fewer chances for deadlocks inside the interpreter itself.
  • Easier C extension development, because many operations can assume single‑threaded access.

When you remove the GIL, you typically introduce more: fine‑grained locks, lock ordering issues, and potential deadlocks. The GIL is one big lock that avoids many of these interpreter‑level problems, at the cost of parallelism.

Best Practices for Designing Python Code With the GIL in Mind

Decide your concurrency model by task type

Use this simple guide:

Workload type Recommended approach
CPU‑bound, heavy computation in Python multiprocessing, C‑extensions, NumPy/Pandas, or consider Cython
I/O‑bound (network, disk) threads or asyncio
Mixed (some CPU, some I/O) Offload CPU parts to processes or C‑code; use threads/asyncio for I/O
Simple scripts and tools Ignore the GIL; write straightforward single‑threaded code first

Keep CPU‑heavy work out of pure Python loops

Patterns to prefer:

  • Use vectorised operations in NumPy and Pandas instead of manual Python loops.
  • Move critical loops to Cython or a C extension when necessary.
  • Use multiprocessing to spread heavy work across multiple cores.

Measure first: use profiling to confirm GIL issues

Do not assume the GIL is your problem. Often, the bottleneck is:

  • Slow database queries.
  • Network latency.
  • Inefficient algorithms.

Before trying to bypass the GIL:

  • Use a profiler to see where your code spends time.
  • Check CPU usage across cores with tools like top or your OS task manager.
  • Look at response times and I/O wait times with monitoring tools.

Only when you know that CPU‑bound work in pure Python is the problem should you change design for the GIL.

Document assumptions about threading in your codebase

To keep your team safe and consistent:

  • Document where threads are used and what they are expected to do.
  • Clearly mark code that assumes “this runs only in one thread”.
  • When you share mutable state between threads, add explicit locks and comments.
  • Note any dependencies on the current CPython GIL behaviour, especially if you plan to test no‑GIL builds in the future.

Summary and Next Steps

Key takeaways

  • The Global Interpreter Lock (GIL) is a single lock in CPython that allows only one thread to execute Python bytecode at a time.
  • The GIL exists mainly for memory safety (reference counting) and interpreter simplicity, but it limits CPU‑bound parallelism with threads.
  • CPU‑bound threads in pure Python do not scale across cores; I/O‑bound threads and asyncio can still give excellent concurrency.
  • The GIL does not make your code automatically thread‑safe and does not guarantee atomicity of operations like x += 1.
  • You can work around GIL limits using multiprocessing, C‑backed libraries, and the right concurrency model for your workload.
  • Python 3.13 introduces an optional no‑GIL build, but most current code still needs to be designed with the GIL in mind.

Where to go next

To put this knowledge into practice, you might explore:

  • Guides on Python threading for I/O‑bound tasks.
  • Tutorials on multiprocessing for CPU‑bound workloads.
  • Beginner’s introductions to asyncio and asynchronous I/O.
  • Articles on how to profile Python code and identify real bottlenecks.
  • Resources about CPython internals and alternative interpreters if you want to go deeper.

Once you understand what the GIL is in Python and how it works, you can design Python systems that use the right tools for the job, avoid common performance traps, and make informed choices as the language and its implementations evolve.

“`