Free-threaded Python (nogil) support#153
Merged
Merged
Conversation
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>
This was referenced Jul 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 isplanning/decisions/2026-07-18-orjson-opt-in-for-free-threading.md.orjsonwas 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_bootstrapcouldn't install on ft at all — not even the pure extras.What changed
orjson→ opt-in extra (lite-bootstrap[orjson]) with a stdlib-jsonfallback in the logging serializer. Core is now zero-dependency pure Python. The GIL-build path is byte-identical whenorjsonis present; the fallback (separators=(",",":"),ensure_ascii=False) matches its compact output for JSON-native values.scripts/ft_smoke.py— notpytest, becausetests/conftest.pyhard-imports opentelemetry, so pytest can't collect withoutotl.import_checker.pyraisedModuleNotFoundError(crashingimport lite_bootstrap) on an incompleteopentelemetry.instrumentationnamespace — now guarded by_safe_find_spec.opentelemetry_instrument.pyunconditionally imported the gRPC OTLP exporter whenever onlyopentelemetry-apiwas present, crashingimport lite-bootstrap[fastmcp]on regular Python too — now gated byis_otlp_grpc_exporter_installed.architecture/free-threading.md,architecture/instruments.md,architecture/bootstrappers.md,README.md,CLAUDE.md, release note1.3.0).Support matrix
logging,sentry,fastapi,faststream(+-metrics)litestar(+litestar-metrics)msgspecgatesPy_GIL_DISABLEDto 3.14+)orjson,otl(grpcio),pyroscope,fastmcp(cffi/otel)planning/deferred.mdwith revisit triggersDeferred (see
planning/deferred.md)is_opentelemetry_installedconflates opentelemetry api/sdk/exporter, solite-bootstrap[fastmcp]withoutotlstill fails to import (at the sdk layer) and setting an OTLP endpoint without the grpc exporter silently skips. A proper fix needs acheck_dependenciesredesign; scoped out to keep this change orjson-focused.Testing
just test: 209 passed, 100% coverage.just lint-ciclean (ruff,ty, planning validator).scripts/ft_smoke.pyverified on real local 3.13t and 3.14t interpreters (GIL disabled), per-version extra sets.🤖 Generated with Claude Code