Python developers have discussed the Global Interpreter Lock, commonly called the GIL, for years.

The reason is simple: the GIL has historically limited how multiple Python threads execute CPU-bound Python code at the same time in CPython.

Python has been moving toward an optional free-threaded execution model, where the GIL can be disabled. Python 3.15 continues that work and makes free-threaded CPython more practical, although it remains an optional build rather than the default Python runtime.

This creates an important question for developers:

If the GIL is disabled, does Python code automatically become faster?

No.

Free-threading changes how Python threads can execute. Whether an application becomes faster depends on the workload, the number of threads, synchronization, native extensions, memory behavior, and how much actual CPU-bound Python work the application performs.

This article explains what free-threading means in Python 3.15 and how to evaluate it in a real application.

What Is the GIL?

The Global Interpreter Lock is a synchronization mechanism used by the standard CPython build.

In a simplified model, multiple Python threads may exist:

Thread 1
Thread 2
Thread 3
Thread 4
     ↓
   GIL
     ↓
CPython interpreter

Only one thread at a time can execute Python bytecode under the traditional GIL model.

This does not mean Python cannot perform concurrent work.

Threads can still be useful when they spend significant time waiting for:

For example:

import threading

def download(url):
    # Network-bound work
    ...

threads = [
    threading.Thread(target=download, args=(url,))
    for url in urls
]

for thread in threads:
    thread.start()

for thread in threads:
    thread.join()

While one thread waits for I/O, another thread can make progress.

The problem becomes more visible with CPU-bound Python code.

CPU-Bound vs I/O-Bound Work

Consider a CPU-heavy function:

def calculate():
    total = 0

    for i in range(50_000_000):
        total += i * i

    return total

If several threads execute this function under the traditional GIL model, they do not get true parallel execution of Python bytecode across CPU cores.

A machine with:

CPU cores: 16
Python threads: 16

does not automatically mean:

16 Python threads
       ↓
16 cores working on Python bytecode

The GIL prevents that in the traditional CPython build.

Free-threaded CPython changes this model.

What Is Free-Threaded Python?

A free-threaded build removes the GIL so multiple Python threads can execute Python code concurrently.

The conceptual difference is:

Traditional CPython

Thread A ──┐
Thread B ──┤
Thread C ──┼── GIL ── Python execution
Thread D ──┘

versus:

Free-threaded CPython

Thread A ──→ CPU core
Thread B ──→ CPU core
Thread C ──→ CPU core
Thread D ──→ CPU core

The second model allows true parallel execution of Python code when the workload and hardware make that useful.

But removing the GIL creates new synchronization requirements inside the interpreter.

The free-threaded implementation therefore uses other mechanisms to maintain memory and object safety.

Python 3.15 and Free-Threading

Python 3.15 continues the free-threaded work introduced in previous Python releases.

The official Python documentation describes free-threaded CPython as an optional build configuration and notes that it is not enabled by default.

This distinction is important.

Installing ordinary Python 3.15 does not mean that the GIL is automatically gone.

You need a free-threaded build.

Depending on the distribution, the executable may be identifiable as a free-threaded build using the t ABI designation.

You can inspect the interpreter configuration with:

import sys

print(sys.version)
print(sys._is_gil_enabled())

On a free-threaded build where the GIL remains disabled, the latter reports False. The API is intended to allow applications and tools to determine whether the GIL is enabled.

The GIL Can Still Be Enabled

Free-threaded CPython does not necessarily mean that the GIL can never exist.

Python's free-threaded implementation provides mechanisms that can enable the GIL in certain situations, including when extension modules that require the GIL are imported.

That means developers should not assume:

Free-threaded build
        =
GIL always disabled

A better model is:

Free-threaded build
        ↓
GIL support removed from the default execution path
        ↓
Some compatibility scenarios may enable it

This matters when evaluating third-party packages.

Why Removing the GIL Is Not a Free Performance Upgrade

It is tempting to think:

No GIL
+
More CPU cores
=
Faster Python

Real applications are more complicated.

A multithreaded application can spend time waiting for:

If your program is mostly waiting on a database, removing the GIL may have little effect.

For example:

Request
 ↓
Database query
 ↓
500 ms waiting
 ↓
Response

The Python interpreter is not the main bottleneck.

Free-threading is much more interesting when several threads perform substantial CPU-bound Python work.

A Simple CPU-Bound Example

Consider:

def calculate(start, end):
    total = 0

    for number in range(start, end):
        total += number * number

    return total

We can divide the work:

0 ───────── 25M
25M ─────── 50M
50M ─────── 75M
75M ─────── 100M

and give each section to a different thread.

Conceptually:

import threading

results = []

def worker(start, end):
    results.append(calculate(start, end))

threads = [
    threading.Thread(
        target=worker,
        args=(0, 25_000_000)
    ),
    threading.Thread(
        target=worker,
        args=(25_000_000, 50_000_000)
    ),
]

With a traditional GIL-enabled interpreter, these threads cannot execute Python bytecode in true parallel fashion.

With a free-threaded interpreter, they can potentially execute concurrently on different CPU cores.

But this example also introduces a shared list:

results.append(...)

That brings us to an important issue.

Free-Threading Makes Synchronization More Important

When multiple threads can execute Python code simultaneously, shared mutable state becomes more important.

Consider:

counter = 0

def worker():
    global counter

    for _ in range(100_000):
        counter += 1

Running this concurrently should not be designed around the assumption that every multi-step operation is automatically safe.

The correct solution is to use explicit synchronization where required.

For example:

import threading

counter = 0
lock = threading.Lock()

def worker():
    global counter

    for _ in range(100_000):
        with lock:
            counter += 1

The lock makes the critical section explicit.

Free-threading therefore changes an important design assumption:

Code that depended on the GIL for accidental synchronization should not be treated as thread-safe.

The GIL Was Never a General Thread-Safety Guarantee

This point is easy to misunderstand.

The GIL did not make arbitrary Python programs thread-safe.

Developers still needed locks and other synchronization mechanisms when sharing mutable state.

However, some existing code may have relied on implementation behavior that happened to be protected by the GIL.

Free-threaded Python exposes those assumptions more clearly.

For example:

shared = {}

def worker(key, value):
    shared[key] = value

If multiple threads access shared data, you should understand the thread-safety guarantees of the specific operation and data structure rather than assuming that the GIL protects everything.

For complex shared state, explicit synchronization is usually easier to reason about.

What Happens to C Extensions?

Third-party native extensions are one of the biggest considerations when evaluating free-threaded Python.

A package containing native code may have assumptions based on the traditional GIL.

Python's free-threading documentation explains that extension modules may need to be updated to support free-threaded execution. Extensions can declare whether they support the free-threaded build, and importing an extension that does not support it can cause the GIL to be enabled.

This creates a compatibility chain:

Python application
       ↓
Python package
       ↓
Native extension
       ↓
Free-threaded support?
       ↓
Yes / No

A single dependency can therefore affect the behavior of the complete application.

Check Your Dependencies Before Migrating

Suppose an application contains:

FastAPI
   ↓
Database driver
   ↓
Native extension
   ↓
System library

Moving the application to a free-threaded interpreter means every relevant component needs to be evaluated.

Check:

Do not benchmark only your application code.

Benchmark the complete application stack.

Free-Threading and asyncio

Free-threading does not replace asynchronous programming.

Consider:

async def fetch_data():
    result = await database_call()
    return result

asyncio is designed primarily around cooperative concurrency and asynchronous I/O.

Free-threading addresses a different problem:

asyncio
    ↓
Efficient concurrent I/O

Free-threading
    ↓
Parallel Python execution across threads

They can also coexist.

For example, an asynchronous application may use worker threads for CPU-heavy tasks.

The important thing is to choose the concurrency model based on the workload.

Free-Threading and multiprocessing

Python's multiprocessing module has traditionally been one way to achieve parallel CPU execution by using separate processes.

Conceptually:

Process 1 → CPU core
Process 2 → CPU core
Process 3 → CPU core
Process 4 → CPU core

Each process has its own interpreter and memory space.

Free-threading provides another model:

One process
   ↓
Multiple Python threads
   ↓
Multiple CPU cores

This can reduce some of the complexity associated with multiple processes, especially when threads need to share memory.

However, shared memory also creates synchronization concerns.

Neither approach is universally better.

Memory Is Another Consideration

Multiple threads operating in one process can share objects directly.

That can be convenient:

Thread A ─┐
Thread B ─┼── Shared objects
Thread C ─┘

But shared objects can also increase contention.

With multiprocessing:

Process A → Memory A
Process B → Memory B
Process C → Memory C

isolation is stronger, but moving data between processes can introduce serialization and communication overhead.

Free-threading changes the tradeoff rather than eliminating it.

How to Benchmark Free-Threaded Python

Do not compare two interpreters by running a single command once.

A meaningful test should measure the actual workload.

For example:

Benchmark
──────────────
Input size
Thread count
Execution time
CPU utilization
Throughput
Memory usage

Test several thread counts:

1 thread
2 threads
4 threads
8 threads
16 threads

You may see something like:

Threads    Relative throughput
1          baseline
2          improved
4          improved
8          improved
16         little additional gain

The exact result depends entirely on the workload.

The important thing is to find where additional concurrency stops providing useful work.

Use a Representative Workload

A benchmark like:

sum(range(10_000_000))

may be useful for experimentation.

It is not enough to decide whether a production service should migrate.

A better test resembles the actual workload:

Production workload
      ↓
Representative dataset
      ↓
Real dependencies
      ↓
Real thread count
      ↓
Realistic request pattern

If your production application spends most of its time waiting on PostgreSQL, benchmark that behavior.

If it performs CPU-heavy image processing, benchmark the image-processing workload.

Measure More Than Execution Time

Suppose free-threaded Python changes:

Execution time:
10 sec → 7 sec

That looks useful.

But also measure:

CPU usage
Memory usage
Lock contention
Throughput
Tail latency
Error rate
Dependency behavior

A change that reduces CPU time but causes significantly higher memory usage may not be appropriate for a constrained production environment.

Similarly, average latency can improve while p99 latency becomes worse.

Production decisions should use the metrics that matter to the application.

A Practical Migration Strategy

Do not replace the production interpreter immediately.

Use stages.

Stage 1: Inventory

Identify:

Python version
Native extensions
Critical dependencies
Thread usage
Shared state
CPU-heavy operations

Stage 2: Run Tests

Run the existing test suite against the free-threaded interpreter.

Look for:

Stage 3: Run Concurrency Tests

Increase thread counts gradually.

1
2
4
8
16

Observe where performance improves and where contention appears.

Stage 4: Profile

Use appropriate CPU and wall-clock profiling.

Python 3.15's profiling improvements and frame-pointer defaults can help system-level profiling on supported platforms.

Stage 5: Canary

Deploy the free-threaded build to a small portion of traffic.

Monitor:

Error rate
Latency
CPU
Memory
Throughput
Dependency failures

Stage 6: Compare

Compare the same workload against the traditional interpreter.

Only then decide whether the free-threaded build provides enough value for the application.

Common Mistakes

Assuming More Threads Always Mean More Performance

Thread count and performance are not directly proportional.

Too many threads can increase contention and scheduling overhead.

Assuming Free-Threading Fixes Every CPU Problem

A slow algorithm remains a slow algorithm.

Changing the interpreter does not replace algorithmic optimization.

Ignoring Native Dependencies

A single unsupported extension can affect free-threaded behavior.

Relying on the GIL for Synchronization

If correctness depends on accidental GIL behavior, the code should be reviewed.

Benchmarking Only One Thread Count

Free-threading is specifically about concurrent execution.

Always test multiple levels of concurrency.

Testing Only Synthetic Code

A microbenchmark can demonstrate potential.

A representative workload determines practical value.

Migrating Production First

Run compatibility and performance testing before changing the production runtime.

Best Practices

When evaluating free-threaded Python 3.15:

  1. Treat it as a separate runtime configuration.

  2. Inventory native dependencies before migration.

  3. Run the complete test suite.

  4. Review code that shares mutable state across threads.

  5. Use explicit synchronization where required.

  6. Benchmark realistic CPU-bound workloads.

  7. Test multiple thread counts.

  8. Measure memory and CPU usage as well as execution time.

  9. Profile both Python and native execution where appropriate.

  10. Test third-party packages independently.

  11. Use a staged or canary deployment for production evaluation.

  12. Keep a GIL-enabled deployment path available during migration.

  13. Compare tail latency, not just averages.

  14. Do not assume free-threading automatically improves I/O-bound applications.

  15. Document which dependencies support the free-threaded runtime.

When Free-Threading Makes Sense

Free-threaded Python is particularly interesting when an application has:

CPU-heavy Python work
+
Multiple independent tasks
+
Multiple CPU cores
+
Thread-based architecture

Examples can include:

The exact benefit depends on the implementation and dependencies.

When It May Not Help Much

Free-threading may provide limited benefit when the workload is primarily:

Network waiting
Database waiting
File I/O
External API calls

For example:

Python
  ↓
HTTP request
  ↓
400 ms waiting

Removing the GIL does not make the remote server respond faster.

Likewise, if your application already uses asynchronous I/O efficiently, changing to free-threaded execution may not provide a meaningful improvement for that particular workload.

Advantages and Limitations

Advantages

Limitations

Summary

Python's move toward free-threading changes one of CPython's most important execution assumptions.

With the traditional GIL-enabled interpreter, multiple threads cannot execute Python bytecode in true parallel fashion. A free-threaded build removes that restriction and allows suitable Python workloads to use multiple CPU cores through threads.

Python 3.15 continues this work, but free-threading remains an optional runtime configuration rather than the default. Native extensions and third-party dependencies are therefore an important part of any migration plan.

The biggest mistake is to treat free-threading as an automatic performance upgrade.

The right question is:

Does my workload contain enough
CPU-bound parallel work to benefit?

If the answer is yes, test it with realistic data, multiple thread counts, real dependencies, and production-like concurrency.

If the application is primarily waiting on databases, networks, or external services, free-threading may not address the actual bottleneck.

The safest approach is to treat free-threading as a runtime option to benchmark and validate—not as a switch that automatically makes every Python application faster.