1 of 66

Free-threaded Python

Past, Present, Future

Thomas Wouters

EuroPython 2026

2 of 66

Who am I?

Thomas Wouters

CPython Core Developer

Steering Council

Meta

Cats

3 of 66

4 of 66

5 of 66

Python

C/C++

6 of 66

Threads

Threads

Thraeds

Threads

Threads

Thraeds twist brian?

Thraeds

Threads

Threads

7 of 66

Waiting on GIL… … … … … … …

8 of 66

When someone forgets to release the GIL

9 of 66

DEADLOCK!!!

10 of 66

Who am I?

Thomas Wouters

CPython Core Developer

Steering Council

Meta

Cats

11 of 66

  • Separate thread of control in a process
  • Shares all memory with other threads
  • For maximizing CPU use
    • Memory access is slower than CPU
    • Less overhead than separate processes

What are threads?

12 of 66

CPUs, caches, memory

13 of 66

14 of 66

  • Separate thread of control in a process
  • Shares all memory with other threads
  • For maximizing CPU use
    • Memory access is slower than CPU
    • Less overhead than separate processes
  • Reasons to support threads:
    • Performance
    • Blocking APIs
    • Third-party code that needs it

What are threads?

15 of 66

  • A single Global Interpreter Lock (per interpreter)
    • Protects object data and reference count

What is the GIL?

16 of 66

struct PyObject {

ssize_t ob_refcnt;

PyTypeObject *ob_type;

}

Python Object Layout

17 of 66

struct PyObject {

PyObject *_ob_next;

PyObject *_ob_prev;

ssize_t ob_refcnt;

PyTypeObject *ob_type;

}

Python Object Layout (with GC)

18 of 66

PyGILState_STATE state = PyGILState_Ensure();

MyObject *myobj = PyList_GetItem(my_global_list, idx);

Py_INCREF(myobj);

myobj->count++;

Py_BEGIN_ALLOW_THREADS

// do something with myobj->data without the GIL held

Py_END_ALLOW_THREADS

Py_DECREF(myobj);

PyGILState_Release(state);

Example Extension Code

19 of 66

  • A single Global Interpreter Lock (per interpreter)
    • Protects object data and reference count
    • Protects CPython internals
    • Very efficient
  • The GIL does not make threads easier
  • The GIL does not protect your Python code
    • Impossible to predict when it is released/reacquired
  • The GIL mostly does not protect your C/C++ extensions

What is the GIL?

20 of 66

PyObject *key = PyList_GetItem(mylist, idx);

Py_INCREF(key);

PyObject *result = PyDict_GetItem(mydict, key);

Py_INCREF(result);

Py_DECREF(key);

return result;

Safe Because of the GIL

21 of 66

PyObject *key = PyList_GetItem(mylist, idx);

PyObject *result = PyDict_GetItem(mydict, key);

Py_INCREF(result);

return result;

Unsafe Despite the GIL

22 of 66

PyObject *key = PyList_GetItem(mylist, idx);

Py_INCREF(key);

PyObject *result = PyDict_GetItem(mydict, key);

Py_DECREF(key);

Py_INCREF(result);

return result;

Unsafe Despite the GIL

23 of 66

PyObject *key = PyList_GetItem(mylist, idx);

Py_INCREF(key);

PyObject *result = PyDict_GetItem(mydict, key);

Py_DECREF(key);

Py_INCREF(result);

return result;

Unsafe Despite the GIL

24 of 66

  • The GIL limits thread usefulness
    • Multi-core hardware is everywhere

Why remove the GIL at all?

25 of 66

26 of 66

  • The GIL limits thread usefulness
    • Multi-core hardware is everywhere
    • "Rewrite it in C/Rust" doesn't always fit
  • Alternatives have their own downsides:
    • multiprocessing, subinterpreters, asyncio
  • Without the GIL, multi-threaded solutions can offer:
    • Higher throughput
    • Lower memory use
    • Lower latency

Why remove the GIL at all?

27 of 66

  • Because threads are hard!

Why is removing the GIL hard?

28 of 66

  • Sharing data between threads is complicated
    • Difficult to get right, very difficult to make efficient
  • CPUs and compilers optimize for speed
    • CPUs cache data
    • CPUs and compilers pre-fetch data
    • CPUs and compilers reorder memory accesses
    • Caching means different cores "see" different data
  • Complicated rules for how this interacts with threads

Why are threads hard?

29 of 66

30 of 66

Thread A

items = list->items

len = list->length

items[len] = item

list->length = len + 1

Interweaving threads

Thread B

items = list->items

len = list->length

items[len] = item

list->length = len + 1

31 of 66

Thread A

items = list->items

len = list->length

items[len] = item

list->length = len + 1

Interweaving threads

Thread B

items = list->items

len = list->length

items[len] = item

list->length = len + 1

32 of 66

Thread A

items = list->items

len = list->length

items[len] = item

list->length = len + 1

Interweaving threads

Thread B

items = list->items

len = list->length

items[len] = item

list->length = len + 1

33 of 66

Thread A

items = list->items

len = list->length

items[len] = item

list->length = len + 1

Interweaving threads

Thread B

items = list->items

len = list->length

items[len] = item

list->length = len + 1

34 of 66

Thread A

items = list->items

len = list->length

items[len] = item

list->length = len + 1

Thread A's Point of View

Thread B

len = list->length

list->length = len + 1

items = list->items

items[len] = item

35 of 66

Thread A

items = list->items

items[len] = item

len = list->length

list->length = len + 1

Thread B's Point of View

Thread B

items = list->items

len = list->length

items[len] = item

list->length = len + 1

36 of 66

Thread A

Thread B

37 of 66

Thread A

items = list->items

items[len] = item

len = list->length

list->length = len + 1

Assignment isn't atomic

Thread B

items = list->items

len = list->length

items[len] = item

list->length = len + 1

38 of 66

  • Prevent partial reads/writes by using atomic operations
  • Prevent reordering with memory fences
    • prevents certain optimizations
    • requires synchronization between threads
  • Acquire-Release semantics force partial ordering
    • prevents reads/writes from being "visible" in the wrong order
  • Sequential Consistency forces total ordering
    • Inherent in a lot of atomic operations

Dealing with shared data

39 of 66

  • Because threads are hard!
  • In Python, everything is an object, and everything is shared
    • Reference counting means everything is shared mutable data

Why is removing the GIL hard?

40 of 66

struct PyObject {

PyObject *_ob_next;

PyObject *_ob_prev;

ssize_t ob_refcnt;

PyTypeObject *ob_type;

}

Python C API

41 of 66

  • Because threads are hard!
  • In Python, everything is an object, and everything is shared
    • Reference counting means everything is shared mutable data
  • CPython's C API relies on the GIL
    • Borrowed references
  • Dicts and lists and objects everywhere!
    • Their performance matters a lot
  • Lots and lots of code out there
    • Changing semantics is hard
    • Performance matters

Why is removing the GIL hard?

42 of 66

  • Greg Stein's patch (1996)
    • Fine-grained locks
    • Much too slow, threads still largely unexplored
  • Trent Nelson's PyParallel (2013)
    • Entirely new API for threads
    • Didn't work with most existing threading code
  • Larry Hastings' GILEctomy (2015)
    • Novel approaches to refcounts
    • Couldn't make it fast enough

The Past

43 of 66

  • Sam Gross's NoGIL (2021), PEP 703 (2023)
  • Replaces the GIL with many different things
    • New Garbage Collector
    • Biased refcounts
    • Deferred refcounts
    • Speculative refcount operations
    • Quiescent-State-Based Reclamation (QSBR)
    • Fine-grained locks
    • New APIs replacing borrowed reference ones

Free-threaded Python

44 of 66

  • Custom allocator (pymalloc) replaced with mimalloc
    • Fast, thread-aware allocator
    • Allocation-size based pools
    • Allows walking all live objects
    • All object allocations must now go through this allocator
  • New GC module
    • Still only for reference cycles
    • Uses mimalloc to walk all live objects
    • Removes _ob_next/_ob_prev pointers

New allocator and GC

45 of 66

  • Based on work done on Swift in 2018
  • Most objects are mostly used from one thread
    • Add "owning thread" to objects
    • Split the refcount in two parts, local and shared
    • local updates are fast, shared updates are atomic
  • Immediate reclamation if shared isn't used
  • Usage from other threads is slower
    • More bookkeeping

Biased Reference Counting

46 of 66

  • Globals are often shared between threads
    • Modules, classes, functions
    • Also often involved in reference cycles
    • Often long-lived, but not immortal
  • The interpreter can defer refcount operations on them
    • Effectively passing around borrowed references
    • Avoids many refcount operations
  • Reclamation is also deferred
    • Interpreter consolidates deferred operations during GC

Deferred Reference Counting

47 of 66

  • Dicts and lists are everywhere
    • Efficient shared access is critical for performance
  • Traditional lookup is racy
    • thread A looks up list->items (a resizeable array)
    • thread A accesses list->items[idx], a Python object
    • thread A INCREFs the object

Lock-free dicts and lists

48 of 66

  • Dicts and lists are everywhere
    • Efficient shared access is critical for performance
  • Traditional lookup is racy
    • thread A looks up list->items (a resizeable array)
    • thread A accesses list->items[idx], a Python object
    • thread B deletes list->items[idx], freeing the object
    • thread A INCREFs the object, now invalid

Lock-free dicts and lists

49 of 66

  • Dicts and lists are everywhere
    • Efficient shared access is critical for performance
  • Traditional lookup is racy
    • thread A looks up list->items (a resizeable array)
    • thread B reallocates list->items
    • thread A accesses list->items[idx], now invalid

Lock-free dicts and lists

50 of 66

  • Dicts and lists are everywhere
    • Efficient shared access is critical for performance
  • Traditional lookup is racy
    • thread A looks up list->items (a resizeable array)
    • thread B reallocates list->items
    • thread A accesses list->items[idx], now invalid
  • But we really want to avoid a slow lock

Lock-free dicts and lists

51 of 66

  • Free-threaded lookup is speculative:
    • Get the item, INCREF it
    • Check if the item in the container is still the same item
    • If not: DECREF the item, try again
  • This only works because the allocator makes it safe!
    • The freed object's memory remains valid
    • If the memory is reused, it's still a Python object
    • The new object's refcount is in the same place in memory
  • Quiescent-State-Based Reclamation for the underlying items array
    • The old container memory remains valid until GC

Lock-free dicts and lists

52 of 66

  • Item assignment also needs to play along
    • items[idx] = item needs to show up before list->len++
  • Can't use functions like memcpy() or memmove()
    • Have to use atomic operations in a loop
  • Fast path only works in specific situations
    • Falls back to slow path with locks
    • Fast path is common enough to make it worth it
  • Still doesn't make borrowed references safe!
    • Use new APIs that return new references

Lock-free dicts and lists

53 of 66

  • Many objects require locking to safely be modified
    • Atomic operations aren't enough
  • Locks around operations that can call arbitrary code are bad
    • They lead to deadlocks
    • Even Py_DECREF can call arbitrary code!
  • Critical Sections: deadlock-free locks (kinda)
    • Release and re-acquire when necessary…
    • … when the GIL would have been released!
  • Emulate the semantics of the GIL, but for one object (or two)

Critical Sections

54 of 66

  • Many things may require Stop-the-World
    • Garbage Collection
    • Global state changes
    • Changing an object's class
    • Imports of unsafe modules (re-enabling the GIL)
  • Repurpose GIL acquire/release calls for thread-attach/detach
  • Stop-the-World stops all attached threads
    • The GIL is now a multi-reader/single-writer lock

Stop-the-World

55 of 66

  • Allows threads to run in parallel while touching Python objects
  • Keeps Python's semantics (mostly) the same
    • No memory model for Python
  • Not nearly as "free" as threading in C, C++, Java, Rust

Free-threaded Python

56 of 66

  • Python 3.13 (2024)
    • Experimental, relatively slow Free-threading
  • Python 3.14 (2025)
    • Supported, Safer, Faster (0-10% slowdown)
  • Python 3.15 (2026)
    • Single Stable ABI for Free-threading and GIL builds (abi3t)
    • Scalability improvements

The Present

57 of 66

  • https://py-free-threading.github.io/porting-extensions/
  • Protect C global variables
    • (or get rid of them)
  • Use critical sections to protect mutable instance data
    • For maximum performance, use lock-free solutions
  • Declare free-threading support in module init
  • Don't get rid of explicit GIL acquire/release calls!
  • Add multi-threaded tests

Migrating your C extensions

58 of 66

PyGILState_STATE state = PyGILState_Ensure();

MyObject *myobj = PyList_GetItemRef(my_global_list, idx);

Py_BEGIN_CRITICAL_SECTION(myobj);

myobj->count++;

Py_END_CRITIAL_SECTION();

Py_BEGIN_ALLOW_THREADS

// do something with myobj->data without the GIL held

Py_END_ALLOW_THREADS

Py_DECREF(myobj);

PyGILState_Release(state);

Migrated Extension Code

59 of 66

  • Think about what makes sense to share between threads
    • Don't bother making things unnecessarily lock-free
    • Uncontended critical sections are pretty fast!
  • Definitely add tests
    • Many thread bugs are trivial to trigger
  • Use ThreadSanitizer
    • Take the time to understand the race reports
    • Don't just silence them by using relaxed loads/stores
  • Stable ABI support (abi3t) is more work

Migration tips

60 of 66

Migrating your Python code

61 of 66

  • You probably don't need any changes

Migrating your Python code

62 of 66

  • You probably don't need any changes
    • If you have no multi-threading bugs
    • And you don't care about scaling
  • Threads in Python don't become more complex
  • Existing bugs are more likely to show up
  • Existing code may be exposed to threads more
  • Scaling well may require changes

Migrating your Python code

63 of 66

  • Ongoing performance and scalability improvements
  • Wide-spread community support
    • https://hugovk.dev/free-threaded-wheels/ (61% of top 360!)
    • Kudos to Meta, Quansight and many contributors
  • Python 3.1x (202x)
    • Free-threading by default
    • Probably driven by downstream distributors
  • Python 3.2x (203y)
    • Free-threading as the only option

The Future

64 of 66

Yhg1s @Yhg1s@social.coop

thomas@python.org twouters@meta.com

Thank You

65 of 66

66 of 66

  • Relaxed read/write
    • No partial reads/writes, no intermediate values
    • No ordering guarantees
  • Load-Acquire
    • Prevents reordering later reads/writes to before the acquire
  • Store-Release
    • Ensures all writes before the release are visible before the release is
  • Atomic add, bitwise and, bitwise or, swap, compare-and-swap
  • Explicit fences (Acquire, Release, Sequential Consistency)

Atomics (that CPython uses)