Skip to content

Commit 0fefb79

Browse files
Berik AshimovBerik Ashimov
authored andcommitted
docs: tier 1 design spec — free-threaded Python 3.13 wheels
Ship cp313t wheels via cibuildwheel and an experimental non-blocking CI job. Mypyc auto-skips on free-threaded builds.
1 parent a24dc8a commit 0fefb79

1 file changed

Lines changed: 174 additions & 0 deletions

File tree

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
# Tier 1 — Free-threaded Python 3.13 wheels (design spec)
2+
3+
**Status:** Draft — awaiting user review
4+
**Date:** 2026-04-18
5+
**Scope:** Ship-only (option A). Build and ship `cp313t` (PEP 703 free-threaded) wheels plus an experimental, non-blocking CI job. No code audit for shared-mutable-state races in this tier.
6+
7+
---
8+
9+
## Goal
10+
11+
Give HawkAPI users installing on CPython 3.13 free-threaded (`python3.13t`) a wheel that installs and imports cleanly, with CI visibility that catches regressions but does not block mainline merges.
12+
13+
Correctness under no-GIL is best-effort at this tier. The `src/hawkapi/_threading.py` helpers already exist for future per-module audits; actually applying them to every shared-state site is deferred.
14+
15+
## Architecture
16+
17+
Two interpreter ABIs ship from one codebase:
18+
19+
| ABI tag | Build variant | Mypyc? | Status |
20+
|-----------|----------------------|--------------------|-------------------------|
21+
| `cp312` | GIL, CPython 3.12 | Yes | Stable (unchanged) |
22+
| `cp313` | GIL, CPython 3.13 | Yes | Stable (unchanged) |
23+
| `cp313t` | Free-threaded 3.13 | **No** (auto-skip) | **Experimental (new)** |
24+
25+
The "should mypyc compile?" decision moves from cibuildwheel env branching into `build_mypyc.is_enabled()`. When the build-time interpreter is free-threaded (`sys._is_gil_enabled() is False`), `is_enabled()` returns `False` even if `HAWKAPI_BUILD_MYPYC=1`, and the hatch hook becomes a no-op. This keeps the CI config declarative — one env var for all builds, the build script self-selects.
26+
27+
Rationale for skipping mypyc on `cp313t`: mypyc-compiled extensions historically require the GIL. Rather than discover this mid-release, we opt out deterministically and document it. If upstream mypyc ships free-threaded support later, flipping the guard is a one-line change.
28+
29+
## Changes
30+
31+
### 1. `build_mypyc.py` — free-threaded auto-skip
32+
33+
Modify `is_enabled()` to additionally check the running interpreter:
34+
35+
```python
36+
def is_enabled() -> bool:
37+
if os.environ.get("HAWKAPI_BUILD_MYPYC", "").strip().lower() not in {"1", "true", "yes", "on"}:
38+
return False
39+
# mypyc-compiled extensions require the GIL; skip on free-threaded builds.
40+
is_gil_enabled = getattr(sys, "_is_gil_enabled", None)
41+
if is_gil_enabled is not None and not is_gil_enabled():
42+
print(
43+
"HAWKAPI_BUILD_MYPYC is set but the interpreter is free-threaded; "
44+
"skipping mypyc compilation.",
45+
file=sys.stderr,
46+
)
47+
return False
48+
return True
49+
```
50+
51+
This is the **load-bearing change**. Every other CI/config edit flows from it: if this works, cibuildwheel needs no per-ABI branching.
52+
53+
### 2. `.github/workflows/wheels.yml` — add `cp313t` to the build matrix
54+
55+
- Extend `CIBW_BUILD` from `"cp312-* cp313-*"` to `"cp312-* cp313-* cp313t-*"`.
56+
- `CIBW_SKIP` already excludes cp36–cp311, pp*, musllinux; `cp313t` is not in the skip list, so nothing to change there.
57+
- `CIBW_ENVIRONMENT` stays `HAWKAPI_BUILD_MYPYC=1`. The build script now self-selects (per change #1).
58+
- `CIBW_TEST_COMMAND` stays the same; the unit suite runs on `cp313t` as a smoke test.
59+
- Free-threaded builds on Windows and macOS are supported by cibuildwheel ≥ 2.21; the current pin (`v2.21.3`) is sufficient.
60+
61+
Expected artefact count increases from 5 to ~10 (5 matrix rows × 2 ABIs, minus any platform that cibuildwheel cannot provide a `cp313t` interpreter for — notably, Linux aarch64 cross-builds may take longer in QEMU).
62+
63+
### 3. `.github/workflows/ci.yml` — new experimental job
64+
65+
Add a job named `test-free-threaded`:
66+
67+
```yaml
68+
test-free-threaded:
69+
name: Test (Python 3.13 free-threaded, experimental)
70+
runs-on: ubuntu-latest
71+
continue-on-error: true
72+
steps:
73+
- uses: actions/checkout@v4
74+
- name: Install uv
75+
uses: astral-sh/setup-uv@v4
76+
with:
77+
enable-cache: true
78+
- name: Set up Python 3.13t
79+
# uv supports free-threaded CPython via the 't' suffix.
80+
run: uv python install 3.13t
81+
- name: Install dependencies
82+
# Use the 3.13t interpreter explicitly.
83+
run: uv sync --python 3.13t --extra dev --extra pydantic
84+
- name: Run unit tests
85+
run: uv run --python 3.13t pytest tests/unit -x --tb=short -q
86+
```
87+
88+
Key decisions:
89+
90+
- **Not added to the main `test` matrix.** Keeping it separate prevents the main matrix from flapping when a 3.13t-specific bug surfaces.
91+
- **`continue-on-error: true`** — job status shown on PR checks but does not block merges. Matches "experimental" framing.
92+
- **Unit tests only** — integration, perf, and memory suites are skipped to keep the job fast. When we graduate to Tier 1-B (full audit), we expand coverage.
93+
- **Fallback if `uv python install 3.13t` fails:** replace with `actions/setup-python@v5` using a `3.13` free-threaded variant. We will use uv by default; if CI reports "3.13t not found", we switch in a follow-up.
94+
95+
### 4. `pyproject.toml` — trove classifier
96+
97+
Add the PEP 779 trove classifier:
98+
99+
```toml
100+
"Programming Language :: Python :: Free Threading :: 1 - Unstable",
101+
```
102+
103+
Positioned after the existing `Programming Language :: Python :: 3.13` entry. `requires-python = ">=3.12"` stays unchanged — free-threaded is a build variant, not a version.
104+
105+
**Verification:** Before merging, confirm the classifier is live on PyPI's classifier index. If PyPI rejects it at upload time, fall back to a comment in the README and remove the classifier until PyPI catches up.
106+
107+
### 5. `docs/guide/free-threaded.md` — new user-facing guide
108+
109+
New page covering:
110+
111+
- **Installation:** `pip install hawkapi` under `python3.13t` ships pure-Python (no mypyc) and works out of the box.
112+
- **Status:** Experimental. The `_threading` module already provides `FREE_THREADED`, `maybe_thread_lock()`, and `maybe_async_lock()` primitives, but internal modules have not been audited for shared mutable state yet.
113+
- **Known limitations:**
114+
- No mypyc perf boost on `cp313t` (upstream-blocked).
115+
- Routing/middleware caches may race under parallel thread-pool executors. Report observed issues; a hardening pass is planned.
116+
- **How to report bugs:** link to GitHub issues with a "free-threaded" label template.
117+
- **Roadmap pointer:** mention that full audit + required CI is tracked as a follow-up (Tier 1-B).
118+
119+
### 6. `docs/index.md` — nav link
120+
121+
Add a link to `guide/free-threaded.md` under the "Guide" section, ordered after `performance.md`.
122+
123+
### 7. `tests/unit/test_threading.py` (optional smoke test)
124+
125+
Add a test verifying `FREE_THREADED` matches `sys._is_gil_enabled()` (only runs where the attribute exists) and the null-lock helpers behave correctly under both branches. Low priority — the helpers are already covered implicitly, but an explicit test makes the free-threaded CI job meaningful even without a real audit.
126+
127+
## Success criteria
128+
129+
1. `cibuildwheel` produces `hawkapi-*-cp313t-cp313t-*.whl` on every OS/arch in the build matrix (Linux x86_64/aarch64, macOS x86_64/arm64, Windows AMD64).
130+
2. `pip install hawkapi` on a `python3.13t` interpreter installs a pure-Python wheel and `import hawkapi` succeeds.
131+
3. `pytest tests/unit` passes under `python3.13t` locally and in CI.
132+
4. The `test-free-threaded` job appears on every PR, passes on a clean main, and is visibly marked "experimental / non-blocking".
133+
5. PyPI accepts the release with the new trove classifier, OR the classifier is cleanly removed without other breakage.
134+
135+
## Out of scope (explicit)
136+
137+
- No audit of shared mutable state (route caches, DI singletons, middleware counters, OpenAPI schema cache, etc.). That audit is Tier 1-B.
138+
- No mypyc-compiled `cp313t` wheels. Reconsider when upstream mypyc ships PEP 703 support.
139+
- No promotion of `3.13t` to a required CI gate.
140+
- No change to `requires-python` or minimum supported Python version.
141+
142+
## Risks and mitigations
143+
144+
| Risk | Mitigation |
145+
|---------------------------------------------------------------|----------------------------------------------------------------------------|
146+
| `uv python install 3.13t` not supported on current uv version | Fallback: `actions/setup-python@v5` with free-threaded Python |
147+
| Trove classifier rejected by PyPI | Remove classifier, merge without it, revisit when PyPI catches up |
148+
| Unit test suite hits a GIL-dependent race under `cp313t` | `continue-on-error: true` absorbs the noise; follow-up issue documents fix |
149+
| cibuildwheel cannot resolve a `cp313t` interpreter on Windows | Expected supported in v2.21+; if not, narrow `CIBW_BUILD` per-OS |
150+
| Build time for `cp313t` wheels doubles the matrix runtime | Accept; artefact upload and cache limit remain within budget |
151+
152+
## Testing approach
153+
154+
- **Local reproduction:** install `python3.13t` via `uv python install 3.13t`, run `pytest tests/unit`.
155+
- **CI:** the new `test-free-threaded` job catches import-time and obvious runtime races on every PR.
156+
- **Release verification:** after a release, manually verify `pip install hawkapi` on a `3.13t` interpreter installs a file named `hawkapi-*-cp313t-cp313t-*.whl`, not the `cp313` wheel.
157+
158+
## Files touched
159+
160+
- `build_mypyc.py` — `is_enabled()` gains a free-threaded check.
161+
- `.github/workflows/wheels.yml` — `CIBW_BUILD` extended with `cp313t-*`.
162+
- `.github/workflows/ci.yml` — new `test-free-threaded` job.
163+
- `pyproject.toml` — new trove classifier.
164+
- `docs/guide/free-threaded.md` — new user guide.
165+
- `docs/index.md` — new nav entry.
166+
- `tests/unit/test_threading.py` — optional smoke test.
167+
168+
## Rollback
169+
170+
If the release produces broken `cp313t` wheels or breaks the main matrix:
171+
172+
1. Revert the commit adding `cp313t-*` to `CIBW_BUILD`. One-line change, safe.
173+
2. Keep the `test-free-threaded` CI job — it's non-blocking and provides ongoing signal.
174+
3. File an issue describing the specific failure mode; Tier 1-B planning picks up from there.

0 commit comments

Comments
 (0)