Skip to content

[Feature]: Support free-threaded CPython without re-enabling the GIL #2443

Open

Description

@rvben

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:

  1. orjson rejects free-threaded builds unless the undocumented ORJSON_BUILD_FREETHREADED build variable is set. See build.rs.
  2. Even when built that way, the extension declares Py_MOD_GIL_USED. Importing it therefore enables the GIL for the process. See src/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 orjson directly.
  • Use orjson on GIL-enabled CPython when it is installed.
  • Use a standard-library JSON implementation on free-threaded CPython.
  • Do not attempt to import orjson on a free-threaded build, since importing an extension that declares Py_MOD_GIL_USED enables the GIL.
  • Preserve the behavior Shiny currently needs, including:
    • dumps() returning bytes
    • loads()
    • sorted keys
    • custom default serialization
    • compatible handling of Unicode, datetimes, and non-finite floats
  • Move orjson to an optional extra such as shiny[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 orjson to declare Py_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 replaces orjson for 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 orjson support: 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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Priority: LowLow-impact bug, docs polish, papercut, or unclear low-severity request.ai-triage:doneMarks an issue whose AI triage workflow is complete.needs-triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions