Skip to main content

conditional-futures

Make multi-threaded concurrency backward- and forward-compatible for the free-threaded future of Python.

Multi-Threading In and Out of Free-Threading

The following is a table of performance results for the execution of a function across each row of a NumPy array (code), with (no GIL) python3.14t and (GIL enabled) python3.14, and with and without ThreadPoolExecutor. Performance improves with python3.14t but degrades with python3.14.

Interpreter Executor Duration
python3.14t None 🟡 0.577
python3.14t ThreadPoolExecutor 🟢 0.34
python3.14 None 🟡 0.544
python3.14 ThreadPoolExecutor 🔴 2.231

ConditionalThreadPoolExecutor provides a single interface to get the best result in either context.

Interpreter Executor Duration
python3.14t None 🟡 0.577
python3.14t ConditionalThreadPoolExecutor 🟢 0.339
python3.14 None 🟡 0.544
python3.14 ConditionalThreadPoolExecutor 🟡 0.532

Introduction

The new free-threaded version of Python (with the GIL disabled) offers extraordinary performance improvements in multi-threading CPU-bound processes. Upgrading your code to take advantage of this performance, however, is problematic. The same multi-threaded code, if run with the GIL enabled, can actually perform significantly worse than single-threaded execution. Even when using a free-threaded interpreter, importing an incompatible C-extension will automatically re-enable the GIL.

For code that will run across many interpreters with or without the GIL, we need interfaces that perform multi-threaded processing only when the GIL is disabled.

The conditional-futures package provides ConditionalThreadPoolExecutor, a drop-in replacement for ThreadPoolExecutor that adapts based on the runtime state of the GIL.

When running under free-threaded Python with the GIL disabled ConditionalThreadPoolExecutor behaves like a normal thread pool. When running under a GIL-enabled build, it falls back on single-threaded execution, potentially avoiding a significant degradation in performance. The same implementation offers optimal performance in all contexts.

Note that, even with the GIL enabled, multi-threading can perform well for I/O-bound processes. ConditionalThreadPoolExecutor is appropriate only for CPU-bound processes that perform worse with the GIL.

Example

The performance of function application on the rows of a 2D NumPy array can be used to show both the benefits of free-threaded Python and the need for ConditionalThreadPoolExecutor.

First, using the free-threaded build of Python 3.14, we can create an array and apply a function to each row of that array. The ipython %time utility is used to measure duration.

$ python3.14t
>>> array = np.arange(100_000_000).reshape(100_000, 1_000)
>>> func = lambda row: (row[row % 2 == 0]**2).sum()
>>> %time _ = np.fromiter((func(row) for row in array), dtype=float, count=array.shape[0])
CPU times: user 580 ms, sys: 662 μs, total: 580 ms
Wall time: 581 ms

Using ConditionalThreadPoolExecutor with this GIL-disabled build of Python we can take advantage of multi-threaded performance on a CPU-bound process: the same routine is almost twice as fast:

>>> with ConditionalThreadPoolExecutor(max_workers=4) as ex:
...     %time _ = np.fromiter(ex.map(func, array), dtype=float, count=array.shape[0])
...
CPU times: user 1.31 s, sys: 98 ms, total: 1.41 s
Wall time: 352 ms

Now, if using the standard Python 3.14 interpreter (with the GIL enabled), ThreadPoolExecutor degrades performance: the same operation takes six times as long!

$ python3.14
>>> array = np.arange(100_000_000).reshape(100_000, 1_000)
>>> func = lambda row: (row[row % 2 == 0] ** 2).sum()
>>> with ThreadPoolExecutor(max_workers=4) as ex:
...     %time _ = np.fromiter(ex.map(func, array), dtype=float, count=array.shape[0])
...
CPU times: user 1.9 s, sys: 2.21 s, total: 4.12 s
Wall time: 2.33 s

Using ConditionalThreadPoolExecutor, one implementation performs optimally in both contexts. Running the same code with python3.14, ConditionalThreadPoolExecutor does not perform as well as with python3.14t, but provides the best option available: single-threaded performance.

>>> with ConditionalThreadPoolExecutor(max_workers=4) as ex:
...     %time _ = np.fromiter(ex.map(func, array), dtype=float, count=array.shape[0])
...
CPU times: user 532 ms, sys: 773 μs, total: 533 ms
Wall time: 533 ms

Installation

pip install conditional-futures

What is New in conditional-futures

1.0.2

Extended documentation.

1.0.1

Extended documentation.

1.0.0

Initial release.

Metadata

Release files for conditional-futures 1.0.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for conditional-futures 1.0.2
File Size Uploaded Provenance Actions
conditional_futures-1.0.2.tar.gz 6.0 kB Without provenance Details for conditional_futures-1.0.2.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for conditional-futures 1.0.2
File Interpreter ABI Platform Actions
conditional_futures-1.0.2-py3-none-any.whl Python 3 none any Details for conditional_futures-1.0.2-py3-none-any.whl

Total release size: 11.6 kB

Release files / conditional_futures-1.0.2.tar.gz

Download URL conditional_futures-1.0.2.tar.gz
Size 6.0 kB
Tags Source
SHA-256 checksum
How to use checksums
accc01561f3278ca069b2b9ace719645933c33569898d7800e8fa8ec39bdcca5
BLAKE2b-256 checksum
How to use checksums
136723dea8ea588e9049b29585f536c4900eac36eef47996c7c34102900d2b37
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.13.7

Release files / conditional_futures-1.0.2-py3-none-any.whl

Download URL conditional_futures-1.0.2-py3-none-any.whl
Size 5.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e98c50faf996ecc273062f9ca6713a8293b78d8f11898dc69e32e0b5fd1710e7
BLAKE2b-256 checksum
How to use checksums
96c2ac093c0cbd9266c2c6139a62a24ea776a63ac06c26e3e5aef0e64f1dcc50
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.13.7

Security for conditional-futures 1.0.2

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

1.0.2 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page