Python 3.13
Table of Contents
Yesterday, Python 3.13 was released, one of the most anticipated versions of the interpreter. The main attraction for me has always been the possibility of running code in Python without GIL (Global Interpreter Lock). The GIL has haunted Python for a long time and has always been a topic of many discussions.
For a long time, there have been attempts to remove the GIL from the interpreter. But what does it block? When we run code with multiple threads in Python, the GIL ensures that common structures in the interpreter are protected between these threads, i.e., against concurrency problems. Several attempts were made, but all left Python slower or only worked for specific cases. A rant by Guido - It isn’t Easy to Remove GIL (https://www.artima.com/weblogs/viewpost.jsp?thread=214235) in response to this post Data Services and Integration, An open letter to Guido van Rossum: Mr Rossum, tear down that GIL! (http://web.archive.org/web/20080907123430/http://blog.snaplogic.org/?p=94). Nothing better than PEP-703 (https://peps.python.org/pep-0703/) to understand how these conversations ended. In reality, they didn’t end, the GIL can be enabled or disabled and each user can choose which version of the interpreter they want to use. Not all is well, as many libraries still need more time to support this option and even stabilize their codes.
You can install Python 3.13 and run the code below to see how it works. If your Linux distribution has not yet made version 3.13 available, you can try using Docker, compiling with pyenv and in a few days using uv. For Mac and Windows, just download the installers from the official website python.org (https://www.python.org/downloads/release/python-3130/).
How to know which interpreter you are using?#
Firstly, download Python 3.13 and run the installer. In the case of Windows, choose custom installation and the option that installs Python3.13t.
After installing Python 3.13, you can check which version you’re using by calling the interpreter in the command line.
The standard Python 3.13 does not display the word experimental after the version.
> python
Python 3.13.0 (tags/v3.13.0:60403a5, Oct 7 2024, 09:38:07) [MSC v.1941 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
On Windows and Mac, the executables of the version without GIL are named differently. On Windows, you use python3.13t to call the version without GIL.
> python3.13t
Python 3.13.0 experimental free-threading build (tags/v3.13.0:60403a5, Oct 7 2024, 09:53:29) [MSC v.1941 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
See the words experimental free-threading appear after the version number.
On Linux, do:
PYTHON_GIL=1 python
or
python -X gil=0
Of course, after installing Python 3.13 and adapting the command to how you call the interpreter. Probably something with python3.13.
A small test#
Let’s run this small test program to see the differences between versions 3.13 with and without GIL. It generates a small processing load and executes it with and without threads. The time of each execution is displayed at the end of each execution. The program detects how many cores you have on your system. You can increase the value of CARGA if you run too fast on your computer.
import time
import threading
import multiprocessing
import contextlib
from rich import print
CARGA = 10_000_000
def trabalho(name):
print(f" + Trabalho {name} iniciando")
z = 0
for i in range(CARGA):
z += i
print(f" - Trabalho {name} acabando")
@contextlib.contextmanager
def cronometro(mensagem):
print(mensagem)
start_time = time.time()
yield
end_time = time.time()
print(f"Tempo: {end_time - start_time}")
def com_threads():
threads = []
for i in range(multiprocessing.cpu_count()):
threads.append(threading.Thread(target=trabalho, args=(i,)))
for thread in threads:
thread.start()
for thread in threads:
thread.join()
def sem_threads():
for z in range(multiprocessing.cpu_count()):
trabalho(z)
if __name__ == "__main__":
with cronometro("Com threads"):
com_threads()
with cronometro("Sem threads"):
sem_threads()
Results with Python 3.13 standard#
Results with Python 3.13 No GIL#
Comparing#
Python | Com threads | Sem Threads |
---|---|---|
3.13 | 8.16193 | 8.50603 |
3.13t | 0.84548 | 9.50476 |
3.12 | 9.20192 | 8.78016 |
All times in seconds.
When comparing the speed between the “Com threads” part of the program between versions 3.13t and 3.13, the difference in performance is enormous. If you open your computer’s performance monitor, you should see all cores being used. If your computer is very fast, increase the CARGA value in the program to see the circus go up in flames.
However, note that in the “Sem Threads” version, the interpreter 3.13t was slower than the standard 3.13.
Also note that the interpreter 3.12 was slower than 3.13 in the “Sem Threads” task and slower than both in the “Com thread”.
Conclusion#
Version 3.13 is an advancement in Python interpreters, not only by the GIL/NOGIL option but also by giving hope to many libraries and workloads that really needed threads. Although the sequential execution results are still not better than the traditional version with GIL, the doors for optimizations are open. We also need to wait for libraries to start publishing versions compatible with 3.13 and supporting Python 3.13 NOGIL interpreter.
Version 3.13 also brings a simple JIT, which can improve greatly in the future and is another promising optimization in the interpreter. Now that it’s in the main version, the community will surely send more code to improve and make the JIT faster. Who knows, we’ll soon reach the level of pypy.
In addition, the Python development team managed to strike a balance between testing new features for special cases without breaking compatibility with the standard version, giving more time for the code to mature and be tested by more projects and developers.