Repository navigation
[Feature]: Support free-threaded CPython without re-enabling the GIL #2443
Description
Category
Performance
Scope
Experimental
Problem
Python 3.14 provides a free-threaded CPython build (3.14t) that can execute Python threads in parallel. Shiny currently cannot be installed and imported on this interpreter while remaining GIL-free because orjson is a mandatory dependency.
A minimal reproduction with Shiny 1.7.0 is:
uv run --python 3.14t --with "shiny==1.7.0" \
python -c 'import sys, shiny; print(shiny.__version__); print(sys._is_gil_enabled())'Using uv 0.9.30 and CPython 3.14.3t, uv attempts to build orjson==3.11.9 from source because no cp314t wheel is published.
There are two additional problems with building it from source:
orjsonrejects free-threaded builds unless the undocumentedORJSON_BUILD_FREETHREADEDbuild variable is set. Seebuild.rs.- Even when built that way, the extension declares
Py_MOD_GIL_USED. Importing it therefore enables the GIL for the process. Seesrc/lib.rs.
Shiny imports orjson during normal startup, so an application cannot currently use Shiny while keeping the GIL disabled.
This request is not for Shiny to automatically parallelize reactive work. The immediate goal is to let applications use free-threaded CPython and explicitly dispatch CPU-bound work to threads without Shiny's JSON dependency enabling the GIL.
Solution
Would the maintainers be open to making orjson optional and adding an internal JSON adapter?
One possible design:
- Add a private JSON adapter used by Shiny instead of importing
orjsondirectly. - Use
orjsonon GIL-enabled CPython when it is installed. - Use a standard-library JSON implementation on free-threaded CPython.
- Do not attempt to import
orjsonon a free-threaded build, since importing an extension that declaresPy_MOD_GIL_USEDenables the GIL. - Preserve the behavior Shiny currently needs, including:
dumps()returning bytesloads()- sorted keys
- custom
defaultserialization - compatible handling of Unicode, datetimes, and non-finite floats
- Move
orjsonto an optional extra such asshiny[fast-json], or use another packaging arrangement preferred by the maintainers. - Add a Python 3.14t CI smoke test that imports and boots Shiny, then verifies that
sys._is_gil_enabled()remains false.
Runtime detection could use:
import sysconfig
is_free_threaded = bool(
sysconfig.get_config_var("Py_GIL_DISABLED")
)There is currently no standardized environment marker that published dependency metadata can use to distinguish GIL-enabled Python 3.14 from Python 3.14t. PEP 780 proposes such a marker, but it remains a draft. This seems to rule out a simple conditional dependency for now.
Alternatives (Optional)
- Force-build
orjson: This does not solve the problem because the resulting extension declares that it uses the GIL. - Patch
orjsonto declarePy_MOD_GIL_NOT_USED: This would be unsafe without first making and validating its native implementation as thread-safe. - Provide an application-level package that impersonates
orjson: This can work as a prototype, but it replacesorjsonfor every dependency in the environment and may break packages that use more of its API. - Maintain a Shiny fork: Feasible, but an upstream JSON adapter would avoid long-term divergence.
- Wait for upstream
orjsonsupport: The project says it may support PEP 703 once stable, but does not provide a timeline and currently has its issue tracker and pull requests disabled. - Use multiple processes: This works today but has different memory-sharing and startup characteristics and does not enable thread-based parallelism within an application process.
Example (Optional)
import sys
import sysconfig
from shiny import App, ui
assert sysconfig.get_config_var("Py_GIL_DISABLED") == 1
assert not sys._is_gil_enabled()
app = App(ui.page_fluid("Running without the GIL"), server=None)After all Shiny imports and application dependencies have loaded, sys._is_gil_enabled() should remain false.
Impact (Optional)
This would allow users to experiment with Python 3.14t and explicitly parallelize CPU-bound application work using threads.
The feature could initially be documented as experimental: Shiny supporting free-threaded startup does not imply that every third-party dependency or application is thread-safe. GIL-enabled installations could retain the current orjson fast path.
Contribution? (Optional)
Yes, I can implement (or help).
Activity