Skip to content

Free-threaded Python (nogil) support#153

Merged
lesnik512 merged 11 commits into
mainfrom
free-threaded-python-support
Jul 18, 2026
Merged

Free-threaded Python (nogil) support#153
lesnik512 merged 11 commits into
mainfrom
free-threaded-python-support

Conversation

@lesnik512

Copy link
Copy Markdown
Member

Summary

Adds free-threaded CPython (nogil) support. Full rationale in the change file: planning/changes/2026-07-18.01-free-threaded-python-support.md; the placement decision is planning/decisions/2026-07-18-orjson-opt-in-for-free-threading.md.

orjson was the sole blocker — it ships no free-threaded wheels and its build refuses to compile on a free-threaded interpreter (orjson v3.11.9 does not support free-threaded Python, verified on 3.14t). Since it was a mandatory core dep used only by the logging serializer, import lite_bootstrap couldn't install on ft at all — not even the pure extras.

What changed

  • orjson → opt-in extra (lite-bootstrap[orjson]) with a stdlib-json fallback in the logging serializer. Core is now zero-dependency pure Python. The GIL-build path is byte-identical when orjson is present; the fallback (separators=(",",":"), ensure_ascii=False) matches its compact output for JSON-native values.
  • Per-version free-threaded CI leg (3.13t + 3.14t) running the standalone scripts/ft_smoke.py — not pytest, because tests/conftest.py hard-imports opentelemetry, so pytest can't collect without otl.
  • Two pre-existing import-safety bugs fixed (surfaced while verifying fastmcp, both not ft-specific, both TDD'd):
    • import_checker.py raised ModuleNotFoundError (crashing import lite_bootstrap) on an incomplete opentelemetry.instrumentation namespace — now guarded by _safe_find_spec.
    • opentelemetry_instrument.py unconditionally imported the gRPC OTLP exporter whenever only opentelemetry-api was present, crashing import lite-bootstrap[fastmcp] on regular Python too — now gated by is_otlp_grpc_exporter_installed.
  • Docs promoted (architecture/free-threading.md, architecture/instruments.md, architecture/bootstrappers.md, README.md, CLAUDE.md, release note 1.3.0).

Support matrix

Surface ft
core, logging, sentry, fastapi, faststream (+ -metrics) ✅ 3.13t + 3.14t
litestar (+ litestar-metrics) ✅ 3.14t only (msgspec gates Py_GIL_DISABLED to 3.14+)
orjson, otl (grpcio), pyroscope, fastmcp (cffi/otel) ❌ documented in planning/deferred.md with revisit triggers

Deferred (see planning/deferred.md)

is_opentelemetry_installed conflates opentelemetry api/sdk/exporter, so lite-bootstrap[fastmcp] without otl still fails to import (at the sdk layer) and setting an OTLP endpoint without the grpc exporter silently skips. A proper fix needs a check_dependencies redesign; scoped out to keep this change orjson-focused.

Testing

  • just test: 209 passed, 100% coverage. just lint-ci clean (ruff, ty, planning validator).
  • scripts/ft_smoke.py verified on real local 3.13t and 3.14t interpreters (GIL disabled), per-version extra sets.

🤖 Generated with Claude Code

lesnik512 and others added 11 commits July 18, 2026 20:07
orjson is the sole blocker for free-threaded CPython: no ft wheels and its
build refuses ft (verified on 3.14t). Make orjson an opt-in extra with a
stdlib-json fallback so core + pure extras install and test on ft; add a
3.13t/3.14t CI leg and a free-threading capability page. otl-grpc and
pyroscope remain ecosystem-blocked (deferred with revisit triggers).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ephemeral plans and subagent-driven-development scratch live here per the
planning convention; keep them out of version control.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
orjson ships no free-threaded wheels and its build refuses ft, yet it was a
mandatory core dep used only by the logging serializer. Move it to an opt-in
`orjson` extra and fall back to the stdlib json accelerator when absent, so
core and the pure extras install on free-threaded CPython. GIL-build output is
byte-identical when orjson is present.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a standalone ft smoke script (bootstrap + serializer fallback) and a CI
matrix leg installing the ft-ready extras. conftest.py hard-imports
opentelemetry, so the ft leg runs the script, not the coverage suite.

orjson/otl/pyroscope are excluded as planned. fastmcp is also excluded: it
transitively pulls in opentelemetry-api, which crashes import_checker.py's
find_spec calls once opentelemetry-instrumentation isn't also present.
litestar is excluded because msgspec gates Py_GIL_DISABLED support to
Python 3.14+, so it fails to build on the 3.13t leg (the CI job shares one
install step across the {3.13t, 3.14t} matrix). Both gaps are tracked in
planning/deferred.md. Also adds a scripts/*.py per-file INP001 ignore since
the smoke script is a standalone entry point, not an importable package.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…opentelemetry stack

find_spec on a dotted name imports its parent first, so
find_spec("opentelemetry.instrumentation.fastapi") raised ModuleNotFoundError
(crashing import lite_bootstrap) when opentelemetry-api was installed without
opentelemetry-instrumentation. Guard both dotted checks with a
_safe_find_spec helper.

Separately, opentelemetry_instrument.py unconditionally imported the grpc
otlp exporter under the coarse is_opentelemetry_installed guard, which only
means opentelemetry-api resolves, not that the exporter package is present.
Give it its own is_otlp_grpc_exporter_installed guard, at both the import and
the bootstrap() use site, so an endpoint configured without the exporter
installed no longer crashes.

Both bugs surface for any environment with opentelemetry-api installed but
not the rest of the stack (e.g. lite-bootstrap[fastmcp], which pulls bare
opentelemetry-api transitively) - not ft-specific, though ft is what
surfaced them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…estar to 3.14t

msgspec still gates Py_GIL_DISABLED to 3.14+, so litestar can't install on
3.13t; move from one shared install step to a per-version extras matrix so
litestar/litestar-metrics can run on 3.14t while 3.13t stays without them.

fastmcp/fastmcp-metrics stay off both legs: cffi (pulled in via fastmcp ->
cryptography) gates free-threaded 3.13 upstream, and on 3.14t a remaining
opentelemetry-sdk-vs-opentelemetry-api activation gap (found while verifying
the previous commit's exporter fix) still crashes import lite_bootstrap with
fastmcp installed. Update planning/deferred.md with the corrected picture for
both fastmcp and litestar.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ant)

Add architecture/free-threading.md with the support matrix reconciled to what
Tasks 1-2b actually shipped: litestar lands on 3.14t only (msgspec's ft gate),
fastmcp stays unsupported on both legs. Update the README dependency claim,
record the single-threaded-init invariant in bootstrappers.md, index the new
page in CLAUDE.md, and add the 1.3.0 release note.

Promote the is_otlp_grpc_exporter_installed guard into instruments.md,
including the silent-OTLP-skip trade-off it leaves open, and fold that into a
cohesive "OTel-stack dependency model" entry in deferred.md alongside the
existing fastmcp entry. Finalize the change file's summary to match the
shipped extra set and the two import-safety bug fixes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop the httptools claim (bare fastapi/faststream don't depend on it) and
update the change file's §2/§3 to the shipped ft CI leg (per-version smoke
script, not just test) and full blocked-extras list, plus the two import-safety
bug fixes found during verification.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Summary and Testing sections still said "two extras" blocked and "just
test" on the ft leg; align them with the shipped per-version smoke-script leg
and full blocked-extras list.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Those two fixes are not ft-specific (they hit any partial-opentelemetry
environment on regular Python); their home is instruments.md and the change
file, not the free-threading capability page. The one ft-relevant fact — the
sdk-vs-api gap blocking fastmcp on 3.14t — already lives in the matrix row.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… BrokerMiddleware

A newer ty flagged that FastStreamTelemetryMiddlewareProtocol /
FastStreamPrometheusMiddlewareProtocol declared only __init__, so a constructed
instance was not assignable to faststream's BrokerMiddleware[Any, Any] at
broker.add_middleware(). Declare the builder __call__ (loosely typed as Any to
stay decoupled from faststream internals). Pre-existing gap, surfaced by tool drift.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lesnik512
lesnik512 merged commit 650206f into main Jul 18, 2026
10 checks passed
@lesnik512
lesnik512 deleted the free-threaded-python-support branch July 18, 2026 19:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant