Important Considerations¶
Before using freethreading, it is important to understand when it is the right choice and what to watch out for.
When to Use freethreading¶
freethreading solves a specific problem: achieving true parallel execution across both standard and
free-threaded Python builds with one codebase. However, this portability comes at the cost of a reduced feature set and
constraints on how workers share data. With that in mind, freethreading works well for:
Projects targeting both standard and free-threaded Python builds
Projects whose needs fall within the common feature set of
threadingandmultiprocessingComputationally intensive code that benefits from true parallelism on standard Python builds, with the added advantage of
threading’s lower overhead on free-threaded builds
For anything else, threading or multiprocessing are likely better choices. If a project already relies on
backend-specific features like shared memory or thread-local storage, then the choice is either to use the
corresponding backend or to adapt the code to the common feature set of both.
Constraints and Pitfalls¶
In addition to general concurrency pitfalls like proper resource cleanup and daemon behavior, freethreading has
a few of its own that stem from supporting consistent behavior across both threading and multiprocessing
backends.
Picklability Requirement¶
Since the multiprocessing backend requires serialization, data passed to workers must be picklable. The library validates this at Worker
creation time, ensuring code works consistently regardless of which backend is active. Here’s a quick example:
from freethreading import Worker
# This raises ValueError - lambdas aren't picklable
worker = Worker(target=lambda: print("Hello!"))
Output:
Traceback (most recent call last):
...
ValueError: Worker arguments must be picklable for compatibility with multiprocessing backend...
Module-level functions are picklable and work with both backends. For instance:
from freethreading import Worker
def greet():
print("Hello!")
if __name__ == "__main__":
worker = Worker(target=greet)
worker.start()
worker.join()
Output:
Hello!
Non-Thread-Safe C Extensions¶
In free-threaded Python builds, the GIL is re-enabled at runtime when a C extension not marked as thread-safe is
loaded. Since freethreading determines its backend once at import time based on the current GIL state, loading
such an extension afterward means the library continues using the threading backend even though the GIL is now
enabled.
This is by design — switching backends mid-execution would cause incompatibilities between primitives created at
different times. To avoid this, import freethreading after loading any C extensions whose thread-safety is
unknown.
Queue.qsize() on macOS¶
On macOS, freethreading.Queue.qsize() raises NotImplementedError with the multiprocessing backend
because sem_getvalue() is not implemented on that platform.
Development Tips¶
freethreading aims for consistent behavior across backends, but understanding the underlying runtime can help
when investigating issues and validating code. Below are a few tips that can help during development.
Checking the Backend¶
Knowing which parallelism backend is being used can be helpful for debugging. Here’s how to check it:
from freethreading import get_backend
print(get_backend())
Output (Standard Python):
multiprocessing
Output (Free-threaded Python):
threading
Testing Across Backends¶
Testing code against both backends ensures it works regardless of which one freethreading selects. It is a great
way to catch some of the pitfalls mentioned above. Here is an example of how to do this using pytest:
import pytest
import sys
@pytest.fixture(params=['threading', 'multiprocessing'])
def backend(request, monkeypatch):
if request.param == 'threading':
monkeypatch.setattr(sys, '_is_gil_enabled', lambda: False)
else:
monkeypatch.setattr(sys, '_is_gil_enabled', lambda: True)
# Clear module cache to re-import with new GIL status
if 'freethreading' in sys.modules:
del sys.modules['freethreading']
import freethreading
return freethreading
def task():
pass
def test_worker(backend):
worker = backend.Worker(target=task)
worker.start()
worker.join()
assert not worker.is_alive()
Output:
$ pytest -v --no-header --tb=no pytest_example.py
=========================== test session starts ===========================
collected 2 items
pytest_example.py::test_worker[threading] PASSED [ 50%]
pytest_example.py::test_worker[multiprocessing] PASSED [100%]
============================ 2 passed in 0.14s ============================
And here is an equivalent example using unittest:
import sys
import unittest
def task():
pass
class BackendTestMixin:
backend = None
original_gil_enabled = None
@classmethod
def setUpClass(cls):
cls.original_gil_enabled = getattr(sys, '_is_gil_enabled', None)
if cls.backend == 'threading':
sys._is_gil_enabled = lambda: False
else:
sys._is_gil_enabled = lambda: True
if 'freethreading' in sys.modules:
del sys.modules['freethreading']
import freethreading
cls.freethreading = freethreading
@classmethod
def tearDownClass(cls):
if cls.original_gil_enabled is None:
if hasattr(sys, '_is_gil_enabled'):
delattr(sys, '_is_gil_enabled')
else:
sys._is_gil_enabled = cls.original_gil_enabled
def test_worker(self):
worker = self.freethreading.Worker(target=task)
worker.start()
worker.join()
self.assertFalse(worker.is_alive())
class TestThreadingBackend(BackendTestMixin, unittest.TestCase):
backend = 'threading'
class TestMultiprocessingBackend(BackendTestMixin, unittest.TestCase):
backend = 'multiprocessing'
Output:
$ python -m unittest -v unittest_example.py
test_worker (unittest_example.TestMultiprocessingBackend.test_worker) ... ok
test_worker (unittest_example.TestThreadingBackend.test_worker) ... ok
----------------------------------------------------------------------
Ran 2 tests in 0.086s
OK