Python 3.13 and the GIL: A Quiet Revolution for Crypto Developers

A Decades-Old Lock Quietly Gets an Off Switch

For most of my career, the Global Interpreter Lock has been one of those Python facts of life you simply accept — the way a one-lane bridge becomes part of your daily commute, irritating but immovable. You learn to navigate around it, build muscle memory, and stop asking why. Then Python 3.13 lands in October 2024, and for the first time since CPython existed, there is a build flag that turns off the GIL. Not as a hack. Not as a fork. As an official, opt-in feature of the reference implementation, backed by PEP 703.

I am building a Solana arbitrage bot, and most of the hot path lives in Rust. The supporting pipeline — backtesting, on-chain data analysis, strategy research — is still Python. So this release matters to me. The question I have to answer is whether free-threaded Python actually changes anything for the kind of work crypto developers do, or whether it is one of those features that looks revolutionary in a release note and turns out to be a footnote in practice.

What the GIL Was Actually Doing

The Global Interpreter Lock is a mutex inside the CPython interpreter that ensures only one native thread executes Python bytecode at a time. It was introduced to keep reference counting simple and to avoid race conditions on shared object state. For a single-core era, it was a reasonable trade — the cost of locking was small, and the benefit of straightforward memory management was large.

The cost became obvious as cores multiplied. By the mid-2010s, a developer on a sixteen-core workstation could spin up sixteen Python threads doing CPU-bound work and watch them politely take turns on a single core. The standard workarounds — multiprocessing for parallelism, async/await for I/O concurrency, or pushing hot loops down into C extensions — all worked, but each had a tax. Multiprocessing paid the price of inter-process communication and pickling. C extensions paid the cost of writing C. Async paid the cost of restructuring your entire program around event loops.

In the AI and scientific computing world, the pressure was particularly acute. PEP 703 quotes a DeepMind researcher describing the situation directly: "We frequently battle issues with the Python GIL at DeepMind... we usually end up translating large parts of our Python codebase into C++. This is undesirable because it makes the code less accessible to researchers." That last sentence is the real motivation. The GIL was not just a performance problem. It was a wall between the people writing prototypes in Python and the people delivering production code in C++. Every team had to staff both sides.

PEP 703 — Five Mechanisms to Replace One Mutex

PEP 703, authored by Sam Gross and funded by Meta, was accepted by the Python Steering Council in July 2023. It is the most ambitious change to the CPython runtime in decades, and it is worth understanding what it actually does because the design tells you where the trade-offs landed.

The proposal replaces the single global lock with five coordinated mechanisms. Biased reference counting is the headline. Every Python object remembers which thread "owns" it. The owning thread uses fast non-atomic operations for reference count updates, while other threads use slower atomic operations on a shared count. Most objects spend most of their lives being touched by one thread, so the fast path dominates the common case.

Immortal objects handle the rest. Small integers, True, False, None, and interned strings get a permanent reference count — Py_INCREF and Py_DECREF become no-ops for them. This eliminates the cache-line bouncing that would otherwise destroy multi-threaded performance the moment any two threads touched None. There is a side effect worth knowing about: interned strings created with sys.intern() are now never deallocated in free-threaded builds. If you intern user-supplied strings, you have a memory leak. The fix is to stop interning untrusted input, which you should have been doing anyway.

Deferred reference counting applies to objects that are frequently referenced from the interpreter's evaluation stack — top-level functions, code objects, modules, class methods. Their true reference count is only computed during garbage collection rather than on every function call. Stop-the-world garbage collection pauses all threads briefly to stabilize reference counts during cycle detection, then resumes before finalizers run to avoid deadlocks. And per-object locking with optimistic fast paths protects dict and list mutations using critical sections, while a switch from pymalloc to mimalloc provides thread-safe allocation with the memory tracking needed to make this work safely.

PEP 703 targeted a 5-6% single-threaded overhead and a 7-8% multi-threaded overhead, per the proposal's own performance section. Those numbers are important to keep in mind because the actual Python 3.13 release missed them — by a lot.

Python 3.13: The First Drop, and the Caveats

Python 3.13 shipped in October 2024 with the free-threaded build as an experimental, opt-in feature. The standard build still uses the GIL. To get the free-threaded version, you build CPython with ./configure --disable-gil, and you get a separate executable named python3.13t. The t is for "threaded." Two binaries, two ABIs, two parallel worlds.

The official documentation does not soft-pedal the trade-offs. The Python team writes plainly that "the free-threaded mode is experimental and work is ongoing to improve it: expect some bugs and a substantial single-threaded performance hit." That hit is real. The specializing adaptive interpreter — the optimization layer Python has been building for several releases to inline and accelerate hot bytecodes — was disabled in the 3.13 free-threaded build. Various benchmarks pegged the single-threaded slowdown at somewhere between 20% and 40%, with some workloads going higher.

The trade also went the other way, dramatically. CodSpeed ran a PageRank benchmark on a sixteen-core ARM64 bare-metal instance, comparing single-threaded, eight-process multiprocessing, and eight-thread multithreading configurations. The multi-threaded python3.13t build was "the fastest" execution for that CPU-bound workload, according to CodSpeed's writeup. When they re-enabled the GIL on the same 3.13t binary, performance collapsed because the specializing interpreter was missing. And multiprocessing — the workaround Python developers have been using for two decades — was "even slower than single-threaded due to the overhead of inter-process communication."

A separate set of benchmarks reported by Towards Data Science showed similar gains for the right kind of workload: prime number finding ran roughly an order of magnitude faster (3.70s down to 0.35s), matrix multiplication with threading saw a comparable improvement (43.95s to 4.56s), and file reading was several times quicker (18.77s to 5.13s). But the same writeup includes the counterweight: in one multiprocessing scenario, the standard interpreter outperformed the free-threaded build, with the author noting "it's important to test thoroughly." That caveat is the honest summary of the 3.13 release. Free-threading is sometimes much faster, sometimes much slower, and you cannot tell which without running your workload.

C extension compatibility is the other shoe. The free-threaded ABI is incompatible with the standard build. Every C extension has to be rebuilt for python3.13t and has to declare GIL support via the Py_mod_gil slot. If a C extension does not declare support, importing it automatically re-enables the GIL — a graceful fallback that protects you from undefined behavior at the cost of silently undoing the thing you were trying to test. The pip toolchain needed version 24.1 or newer to handle the new ABI. The NumPy, SciPy, PyTorch, and Pandas of the world have been working through their own free-threading porting efforts.

Python 3.14: From Experiment to Officially Supported

Python 3.14 marks the transition to Phase II of the GIL rollout. The free-threaded build is no longer labeled experimental — it is officially supported. The specializing adaptive interpreter, the missing piece that caused most of the 3.13 single-threaded regression, is back on for the free-threaded build. According to the 3.14 release notes, single-threaded overhead has come down from the 20-40% range of 3.13 to somewhere between 5% and 10% on most platforms, with measurements varying from roughly 1% on macOS aarch64 to about 8% on x86-64 Linux. That is a major improvement and brings the actual release into the same ballpark as PEP 703's original targets.

Early multi-threaded benchmarks on four-core machines show two-to-four-times speedups for CPU-bound workloads. That is not the ten-times-faster headline number some 3.13 benchmarks produced, but it is more honest. A two-to-four-times improvement on four cores is exactly what you would expect from a well-implemented free-threaded interpreter, accounting for synchronization overhead and shared resource contention.

There are behavioral changes worth flagging. In 3.14's free-threaded build, threads inherit the caller's context by default (sys.flags.thread_inherit_context is true) — a flag previously set differently in the standard build. Warning filters now use context variables rather than a global list, which makes them thread-safe. And iterator safety is still a sharp edge: accessing the same iterator from multiple threads can still produce duplicate or missing elements. The Python documentation is clear that built-in container types like dict, list, and set have internal locks for individual operations, but concurrent modification is still not safe. Free-threaded does not mean lock-free for application code. It means the lock you used to inherit for free is gone, and you have to put your own back where it matters.

The three-phase rollout that PEP 703 mapped out is now visible in the release notes. Phase 1 was Python 3.13 with the compile flag and the experimental tag. Phase 2 starts with 3.14: same compile flag, but officially supported. Phase 3 — eventually — would make free-threading the default and put the GIL behind an opt-in flag. The Python Steering Council was careful to write in a proviso: gradual rollout, break as little as possible, and willingness to "roll back changes if too disruptive—potentially rolling back all of PEP 703 if necessary." Translating that out of policy language: this is still reversible if it goes badly. The Python community is taking the risk, but they are not taking it blindly.

What This Means for the MEV Hot Path

Here is where I have to be honest with myself. Free-threaded Python does not change the calculus that pushed me to Rust for the bot's execution path. The numbers that drove that decision are still the numbers.

A practitioner benchmark by Solid Quant on Medium, comparing JavaScript, Python, and Rust for MEV bots, measured HTTP provider creation at 8 microseconds in Rust versus 1,100 microseconds — over a millisecond — in Python. A batch multicall of 3,774 calls came back in 170 ms in Rust versus 1,600 ms in Python. Those gaps are not GIL gaps. They are runtime gaps, allocation gaps, and ecosystem gaps. Removing the GIL does not change how fast Python's HTTP client constructs a connection object, and it does not change how much work the interpreter does per bytecode operation.

The Solid Quant writeup makes a point I keep coming back to: "Network latency emerged as the primary performance bottleneck across all languages, rather than language-specific overhead." That is true at one scale. When you are talking to an RPC endpoint across a regional network, your fifty milliseconds of network round-trip dwarfs your one millisecond of HTTP provider setup. But MEV is not played at one scale. When you are co-located with infrastructure and racing to submit transactions inside the 200-millisecond window that an MEV infrastructure analysis describes as the threshold for capturing meaningful arbitrage, every millisecond of language overhead is a millisecond fewer for the network. That same analysis describes one trading operation that found 400 ms of node latency was costing them around 40% of their arbitrage captures, and that switching infrastructure pushed their success rate from 60 to 85 profitable trades per 100 attempts. Language overhead lives in the same budget.

Free-threading is also pointed at the wrong problem for the hot path. MEV bot execution is dominated by I/O — network round-trips to RPC endpoints, signature verification, transaction serialization — not by CPU-bound parallel computation. The async/await model Python already had was a reasonable fit for that pattern. Threads competing for the GIL was not really the bottleneck on the execution side, because the bot was rarely doing CPU-bound work for long enough to be limited by it. Removing the GIL helps workloads that are bound by Python bytecode executing across multiple cores. Hot-path MEV execution is not that.

For context, the market that this hot path competes in is not small. An analysis reports Q2 2025 Solana MEV-related revenue at $271 million — close to 40% of total MEV earnings across major chains — with Ethereum at $129 million for the same quarter. That is the size of the prize that the 200-millisecond latency budget exists to capture. The competitive pressure is real, and the runtime you pick has to earn its place against it.

Where Python Still Earns Its Keep in My Pipeline

The pipeline around the bot is a different story. Backtesting, on-chain data analysis, strategy research, parameter sweeps — this is where Python lives in my stack, and this is where free-threading actually changes things.

A backtest that scans thousands of historical blocks looking for arbitrage opportunities is embarrassingly parallel. Today, you parallelize it with multiprocessing and pay the cost of pickling state across processes, of warm-starting workers, of coordinating shared memory. Free-threading lets you write the same backtest as a ThreadPoolExecutor over the same in-process objects — no pickling, no warm-start, no shared-memory dance. For an exploratory workload where I want to test thirty configurations in parallel against the same loaded dataset, that is a real ergonomic win, and the 5-10% single-threaded overhead in 3.14 is a price I am willing to pay for it.

Machine learning inference for signal generation is another sweet spot. If I want to run a price-prediction model in one thread while another thread scans pool states, the GIL used to make that a juggling act. Free-threading removes that juggling. The model can run on its own thread, the scanner can run on its own thread, and they share the same in-process data structures without any serialization tax. The DeepMind quote that drove PEP 703 was about exactly this pattern at a larger scale — researchers wanting to compose threaded ML pipelines without dropping to C++.

On-chain data ingestion for analysis — pulling historical block data, parsing transactions, building indices — also benefits. These are workflows where the data set is too large to make multiprocessing cheap, the operations are CPU-bound enough to be GIL-limited, and the engineering simplicity of threads beats the engineering complexity of processes. The free-threaded build has higher memory overhead — non-GC object headers are larger (the docs note that None jumps from 16 bytes to 32 bytes on AMD64), immortal strings never deallocate, and the lock-free data structures defer memory reclamation through a quiescent-state mechanism — but for analytics workloads running on workstations with plenty of RAM, those costs are tolerable.

What I will not do, at least not yet, is rewrite the hot path in free-threaded Python. The ecosystem is still warming up. C extensions are still being ported. The 5-10% single-threaded overhead in 3.14 is small but not zero, and for a workload where the runtime itself is the bottleneck, that overhead matters. The roadmap to Phase 3 — where free-threading becomes the default — will take years, and the steering council has explicitly reserved the right to roll the whole thing back if it does not work out. I would rather watch the rollout from a project that does not depend on the runtime for its competitive edge.

The Honest Read on a Major Release

PEP 703 is one of the most significant changes to Python since the 2-to-3 transition, and it has the potential to reshape how people write concurrent Python over the next decade. But it is not a magic wand, and it does not erase the trade-offs that make a language a good fit for a particular workload.

For MEV bots running on the hot path, the question of language choice is settled by the network and the runtime, not by the GIL. Removing the GIL closes one gap with statically-typed languages while leaving the other gaps — memory layout, allocation strategy, dispatch overhead — exactly where they were. Rust is still Rust. Python is still Python.

For the pipeline around the bot — the analysis, the research, the strategy development — free-threading is genuinely useful. It removes a workaround tax I have been paying for years without realizing how much of my code structure was warped by it. As the C extension ecosystem catches up and 3.14 stabilizes, more of the tools I use day-to-day will start to take advantage of threads in ways they could not before. That is the part of this release I am actually excited about.

The Steering Council's proviso is the line I want to remember. "Gradual rollout. Break as little as possible. Roll back changes if too disruptive—potentially rolling back all of PEP 703 if necessary." That is the right disposition to have toward a change this big, both for the people maintaining the language and for the people building on top of it. Pay attention, but do not overcommit until the experiment finishes running.

Key Takeaways

  • Python 3.13, released in October 2024, introduced the first official free-threaded build of CPython — opt-in via a --disable-gil compile flag and a separate python3.13t executable.
  • PEP 703 replaces the single Global Interpreter Lock with five coordinated mechanisms: biased reference counting, immortal objects, deferred reference counting, stop-the-world garbage collection, and per-object locking — backed by a switch from pymalloc to mimalloc.
  • Python 3.13's free-threaded build carried a 20-40% single-threaded penalty because the specializing adaptive interpreter was disabled; Python 3.14 restored it and brought single-threaded overhead down to roughly 5-10%.
  • For MEV bot hot paths, free-threading does not close the language gap with Rust — the dominant costs are network latency, HTTP setup, and per-operation runtime overhead, not GIL contention.
  • For the pipeline around the bot — backtesting, on-chain analysis, strategy research, parallel parameter sweeps — free-threading is a meaningful ergonomic improvement and worth adopting as the C extension ecosystem catches up.

Disclaimer

This article is for informational and educational purposes only and does not constitute financial, investment, legal, or professional advice. Content is produced independently and supported by advertising revenue. While we strive for accuracy, this article may contain unintentional errors or outdated information. Readers should independently verify all facts and data before making decisions. Company names and trademarks are referenced for analysis purposes under fair use principles. Always consult qualified professionals before making financial or legal decisions.