What
The freethreaded CI job (.github/workflows/_checks.yml) sets DISABLE_SQLALCHEMY_CEXT_RUNTIME=1,
and docs/introduction/installation.md tells users to set the same variable for a genuinely
GIL-free process. Both are a workaround for an upstream gap, and both should come out once upstream
closes it.
Why it is there
SQLAlchemy ships cp314t wheels, but its Cython extensions do not declare Py_MOD_GIL_NOT_USED, so
CPython force-re-enables the GIL process-wide on import sqlalchemy. A free-threaded wheel is
necessary but not sufficient for a GIL-free run. asyncpg and pydantic-core are unaffected —
SQLAlchemy is the only offender in the dependency graph.
Reproduced on CPython 3.14.7t against SQLAlchemy 2.0.52 (current latest):
$ python -c "import sys, sqlalchemy; print(sys._is_gil_enabled())"
<frozen importlib._bootstrap>:491: RuntimeWarning: The global interpreter lock (GIL) has been
enabled to load module 'sqlalchemy.cyextension.collections', which has not declared that it can
run safely without the GIL.
True
$ DISABLE_SQLALCHEMY_CEXT_RUNTIME=1 python -c "import sys, sqlalchemy; print(sys._is_gil_enabled())"
False
CPython emits the diagnostic itself, naming sqlalchemy.cyextension.collections. The variable is
SQLAlchemy's own documented switch to the pure-Python implementations of the same behaviour — not a
hack — but the fallback is slower, so it is a real (if small) cost we are paying only to keep the
GIL-off assertion meaningful.
Why it matters at all
The library runs correctly on 3.14t either way; the GIL state does not change outbox semantics. It
matters for a user running other threaded code in the same process, whose parallelism SQLAlchemy's
cyextensions would otherwise kill process-wide. That is the guarantee docs/introduction/installation.md
advertises, and the CI assertion is what keeps it honest.
Done when
- SQLAlchemy's Cython extensions declare
Py_MOD_GIL_NOT_USED.
- Re-running the
freethreaded job's assertion without the variable still reports
sys._is_gil_enabled() is False.
- Then: remove the
env: entry from the job, the caveat block in
docs/introduction/installation.md, and the corresponding paragraph in
docs/adr/0002-free-threading-is-compat-only.md.
Upstream's free-threading work is sqlalchemy/sqlalchemy#12881, which covered building and testing
cp314t wheels and is closed; declaring the extensions themselves GIL-safe was not part of it, so
there is no upstream tracking issue to watch yet. Worth re-checking on each SQLAlchemy minor.
Note
The docs caveat also mentions that the same problem applies to foreign-broker clients used with the
relay feature (for example aiokafka). That half is not ours to fix and stays regardless.
What
The
freethreadedCI job (.github/workflows/_checks.yml) setsDISABLE_SQLALCHEMY_CEXT_RUNTIME=1,and
docs/introduction/installation.mdtells users to set the same variable for a genuinelyGIL-free process. Both are a workaround for an upstream gap, and both should come out once upstream
closes it.
Why it is there
SQLAlchemy ships
cp314twheels, but its Cython extensions do not declarePy_MOD_GIL_NOT_USED, soCPython force-re-enables the GIL process-wide on
import sqlalchemy. A free-threaded wheel isnecessary but not sufficient for a GIL-free run.
asyncpgandpydantic-coreare unaffected —SQLAlchemy is the only offender in the dependency graph.
Reproduced on CPython 3.14.7t against SQLAlchemy 2.0.52 (current latest):
CPython emits the diagnostic itself, naming
sqlalchemy.cyextension.collections. The variable isSQLAlchemy's own documented switch to the pure-Python implementations of the same behaviour — not a
hack — but the fallback is slower, so it is a real (if small) cost we are paying only to keep the
GIL-off assertion meaningful.
Why it matters at all
The library runs correctly on 3.14t either way; the GIL state does not change outbox semantics. It
matters for a user running other threaded code in the same process, whose parallelism SQLAlchemy's
cyextensions would otherwise kill process-wide. That is the guarantee
docs/introduction/installation.mdadvertises, and the CI assertion is what keeps it honest.
Done when
Py_MOD_GIL_NOT_USED.freethreadedjob's assertion without the variable still reportssys._is_gil_enabled() is False.env:entry from the job, the caveat block indocs/introduction/installation.md, and the corresponding paragraph indocs/adr/0002-free-threading-is-compat-only.md.Upstream's free-threading work is sqlalchemy/sqlalchemy#12881, which covered building and testing
cp314twheels and is closed; declaring the extensions themselves GIL-safe was not part of it, sothere is no upstream tracking issue to watch yet. Worth re-checking on each SQLAlchemy minor.
Note
The docs caveat also mentions that the same problem applies to foreign-broker clients used with the
relay feature (for example
aiokafka). That half is not ours to fix and stays regardless.