Support free-threaded Python 3.13 build
Is your feature request related to a problem?
Python 3.13 will be available as an experimental free-threaded build that has the GIL disabled. It should hopefully be possible to build aiohttp on the free-threaded build with an updated Cython. I just did so (with some makefile hacking so the build uses a prerelease cython):
Python 3.13.0rc1 experimental free-threading build (main, Aug 14 2024, 13:20:46) [Clang 15.0.0 (clang-1500.3.9.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import aiohttp
<frozen importlib._bootstrap>:488: RuntimeWarning: The global interpreter lock (GIL) has been enabled to load module 'aiohttp._helpers', which has not declared that it can run safely without the GIL. To override this behavior and keep the GIL disabled (at your own risk), run with PYTHON_GIL=0 or -Xgil=0.
Describe the solution you'd like
I don't know what thread safety issues exist in aiohttp, but assuming any C/C++ code is thread safe and the cython code is thread safe, you can declare the modules support free-threading by following https://py-free-threading.github.io/porting/.
Describe alternatives you've considered
Not supporting it?
Related component
Server, Client
Additional context
No response
Code of Conduct
- [X] I agree to follow the aio-libs Code of Conduct
If you'd like to help, I think the first thing would be to add a new test run to the CI, so we have some test coverage over this new approach. But, I'd assume the Cython code is likely thread-safe as it's just used for parsers.
Probably need the current 3.13 tests passing first though, which will hopefully happen next week after uvloop publish a 3.13 wheel.
I think that getting our in-org hosted deps to run CI / make builds might be another blocker. I think that aiohttp's CI should make use of those wheels rather than compiling them.
3.13 is fully tested and passing now, if anyone wants to look at adding the free-threading testing.
@Yushu2606 what's your point? That doesn't seem related to the discussion here.
@Yushu2606 you probably aren't building with a nightly cython. Right now there isn't a tagged cython version or release that supports free-threading.
Please add cython version or release that supports free-threading.
Is this forgotten?
No, plenty of work has been done on aiohttp dependencies, so looks like it's shaping up for a release in the near future. Plenty of the ecosystem is still not ready for free-threaded, so it's not been a trivial change (e.g. needing to use an alpha release of Cython).
i'm interested, too. is there a list of free-threaded blockers that need to be addressed?
welp, i just built it for free-threaded on Windows. here's how I did it, i'm sure there will be opinions =) there's surely a less hacky way to go with the macros...
i've only tested the http client, which works, but i get this msg, any pointers on how to remove this warning would be appreciated:
<frozen importlib._bootstrap>:488: RuntimeWarning: The global interpreter lock (GIL) has been enabled to load module 'aiohttp._http_writer', which has not declared that it can run safely without the GIL. To override this behavior and keep the GIL disabled (at your own risk), run with PYTHON_GIL=0 or -Xgil=0.
here's what i changed:
setup.py:
reason: cython's maze of #defs and #undefs
macros = [("GRAALVM_PYTHON", "1"), ("Py_GIL_DISABLED", "1")]
extensions = [
Extension("aiohttp._websocket.mask", ["aiohttp/_websocket/mask.c"], define_macros=macros),
Extension(
"aiohttp._http_parser",
[
"aiohttp/_http_parser.c",
"aiohttp/_find_header.c",
"vendor/llhttp/build/c/llhttp.c",
"vendor/llhttp/src/native/api.c",
"vendor/llhttp/src/native/http.c",
],
define_macros=[("LLHTTP_STRICT_MODE", 0),("GRAALVM_PYTHON", "1"), ("Py_GIL_DISABLED", "1")],
include_dirs=["vendor/llhttp/build"],
),
Extension("aiohttp._http_writer", ["aiohttp/_http_writer.c"], define_macros=macros),
Extension("aiohttp._websocket.reader_c", ["aiohttp/_websocket/reader_c.c"], define_macros=macros),
]
_http_parser.c, line 30742:
reason: aiohttp/_http_parser.c(30744): error C2039: 'ob_refcnt': is not a member of '_object'
#if !CYTHON_USE_TP_FINALIZE
assert(Py_REFCNT(self) > 0);
// if (likely(--self->ob_refcnt == 0)) {
// return;
// }
{
Py_ssize_t refcnt = Py_REFCNT(self);
_Py_NewReference(self);
__Pyx_SET_REFCNT(self, refcnt);
}
websocket\reader_c.c line 18485:
reason: aiohttp/_websocket/reader_c.c(18487): error C2039: 'ob_refcnt': is not a member of '_object'
#if !CYTHON_USE_TP_FINALIZE
assert(Py_REFCNT(self) > 0);
// if (likely(--self->ob_refcnt == 0)) {
// return;
// }
{
Py_ssize_t refcnt = Py_REFCNT(self);
_Py_NewReference(self);
__Pyx_SET_REFCNT(self, refcnt);
}
Well, the warning is because it doesn't support free-threading. Fixing the warning is done by supporting free-threading, i.e. what this issue tracks. I think multidict and frozenlist have free-threading now, and yarl I believe is ready to merge and release. That'll probably be all the dependencies sorted, then it's just aiohttp itself that needs a similar update, so seems like it's getting close now.
Isn't there some optional DNS/speedup stuff on top of the mandatory deps?
I would assume an optional dependency is not a blocker to us. Maybe pycares will need an update? But, people can use it without aiodns until that becomes supported.
It looks like pycares uses CFFI, so that might require using our fork of CFFI to get that working. See https://github.com/python-cffi/cffi/pull/143#issuecomment-2581049170. I'll open an issue over at the pycares repo.
Well, the warning is because it doesn't support free-threading. Fixing the warning is done by supporting free-threading, i.e. what this issue tracks.
right, which was what i was asking about. in pybind11 it's a simple one line code change to disable GIL for an entire module. do we have something simple like that available to us?
i just looked it up, and it seems the answer is no for now. setting PYTHON_GIL=0 is still the method listed in cython docs.
is still the method listed in cython docs
There is a way to declare free-threaded support in a cython module:
https://py-free-threading.github.io/porting-extensions/#__tabbed_1_3
You need to use a nightly Cython for now. Hopefully we’ll see a Cython 3.1 release soon! Or at least a beta…
That said, there’s not much point in doing that here until aiohttp’s dependencies do it first.
i've barely tested because i only need the http client, but i'm using it in my free-threaded project right now. i'm new around here, but it's at least usable for some people right this moment. i have multidict and yarl installed in my 3.13t venv and i didn't do anything special...(except maybe have a build environment....no idea if i built them or not)
edit...multidict has a published 3.13t wheel, but it looks like yarl doesn't, which means i must have successfully built it on windows. and i have absolutely no idea if my code has actually tried using it yet...
@webknjaz @Dreamsorcerer @asvetlov HF Datasets depends on the package. Dataset is used in model training and quantization. At this moment, the entire HF train/quant eco system is collasping on 3.13t support due to aiohttp unable to compile under free-threading env. Thanks!
@electroglyph @ngoldbaum I am desperate for a solution so I can get aiothttp compiled as my GPTQModel project depends on pypi: datasets and datasets massively embed/uses aiothttp in all it's code it's not trivial to find a replacement. Thanks for any pointers.
@Qubitium feel free to contribute supporting this runtime.
@Qubitium feel free to contribute supporting this runtime.
@webknjaz I am asking for help. I have contributed to many oss projects. I can't fix everyone's compat issues and bugs. If you can tell me which specific parts that needs fixing, starting with basic compile, I am willing to spend some time fixing it.
We need list of all broken deps and checkboxes, it will be the best on current stage.
I think all required dependencies are done now. Not sure I've seen a PR to aiohttp yet though, so probably just aiohttp remaining now.
I'm still not familiar with exactly what changes are needed, but maybe looking at the other PRs can help, e.g.: https://github.com/aio-libs/multidict/pull/1015/files https://github.com/aio-libs/yarl/pull/1456/files https://github.com/aio-libs/frozenlist/pull/618/files
pycares (aiodns) is also still missing support, but that is an optional dependency.
I tried setting up aiohttp and running the tests on the free-threaded build today and ran into some issues, mostly around setting up test dependencies. Here are some notes:
A number of test dependencies in turn depend on CFFI, which does not yet support the free-threaded build. I have a PR open to CFFI to add support (https://github.com/python-cffi/cffi/pull/178) and hopefully that will be fixed soon, allowing myself and others to add support in reverse dependencies of CFFI.
For now to get things working you'll need to install CFFI from that PR branch and then manually build test dependencies of aiohttp that depend on CFFI with build isolation disabled. Additionally, you'll need to build python-zstandard from the PR branch that adds support: https://github.com/indygreg/python-zstandard/pulls. It might make sense to use the backports.zstd package instead of zstandard: https://pypi.org/project/backports.zstd/ due to maintenance issues in python-zstandard.
For uvloop, see https://github.com/MagicStack/uvloop/issues/642#issuecomment-3084563094.
With all that, I was able to run the tests. I had to force-disable the GIL and hack the tests to not crash on warnings due to a warning coming from pytest-xdist about --rsyncdir. After that I saw two failures:
___________________________________________ test_testcase_no_app ___________________________________________
[gw7] darwin -- Python 3.13.5 /Users/goldbaum/.pyenv/versions/3.13.5t/bin/python3.13
[XPASS(strict)] https://github.com/pytest-dev/pytest/issues/13546
------------------------------------------- Captured stdout call -------------------------------------------
=========================================== test session starts ============================================
platform darwin -- Python 3.13.5, pytest-7.4.0, pluggy-1.6.0
codspeed: 4.0.0 (disabled, mode: walltime, callgraph: not supported, timer_resolution: 41.7ns)
rootdir: /private/var/folders/nk/yds4mlh97kg9qdq745g715rw0000gn/T/pytest-of-goldbaum/pytest-1/popen-gw7/test_testcase_no_app0
plugins: xdist-3.7.0, codspeed-4.0.0, timeout-2.4.0, cov-4.1.0, mock-3.14.1, run-parallel-0.4.5.dev0, hypothesis-6.135.32
collected 1 item
Collected 0 items to run in parallel
test_testcase_no_app.py E [100%]
================================================== ERRORS ==================================================
_______________________________ ERROR at setup of InvalidTestCase.test_noop ________________________________
cls = <class '_pytest.runner.CallInfo'>
func = <function call_runtest_hook.<locals>.<lambda> at 0x500283a5780>, when = 'setup'
reraise = (<class '_pytest.outcomes.Exit'>, <class 'KeyboardInterrupt'>)
@classmethod
def from_call(
cls,
func: "Callable[[], TResult]",
when: "Literal['collect', 'setup', 'call', 'teardown']",
reraise: Optional[
Union[Type[BaseException], Tuple[Type[BaseException], ...]]
] = None,
) -> "CallInfo[TResult]":
"""Call func, wrapping the result in a CallInfo.
:param func:
The function to call. Called without arguments.
:param when:
The phase in which the function is called.
:param reraise:
Exception or exceptions that shall propagate if raised by the
function, instead of being wrapped in the CallInfo.
"""
excinfo = None
start = timing.time()
precise_start = timing.perf_counter()
try:
> result: Optional[TResult] = func()
/Users/goldbaum/.pyenv/versions/3.13.5t/lib/python3.13t/site-packages/_pytest/runner.py:341:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/Users/goldbaum/.pyenv/versions/3.13.5t/lib/python3.13t/site-packages/_pytest/runner.py:262: in <lambda>
lambda: ihook(item=item, **kwds), when=when, reraise=reraise
/Users/goldbaum/.pyenv/versions/3.13.5t/lib/python3.13t/site-packages/pluggy/_hooks.py:512: in __call__
return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
/Users/goldbaum/.pyenv/versions/3.13.5t/lib/python3.13t/site-packages/pluggy/_manager.py:120: in _hookexec
return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
/Users/goldbaum/.pyenv/versions/3.13.5t/lib/python3.13t/site-packages/pluggy/_manager.py:475: in traced_hookexec
return outcome.get_result()
/Users/goldbaum/.pyenv/versions/3.13.5t/lib/python3.13t/site-packages/pluggy/_manager.py:472: in <lambda>
lambda: oldcall(hook_name, hook_impls, caller_kwargs, firstresult)
/Users/goldbaum/.pyenv/versions/3.13.5t/lib/python3.13t/site-packages/pluggy/_callers.py:53: in run_old_style_hookwrapper
return result.get_result()
/Users/goldbaum/.pyenv/versions/3.13.5t/lib/python3.13t/site-packages/pluggy/_callers.py:38: in run_old_style_hookwrapper
res = yield
/Users/goldbaum/.pyenv/versions/3.13.5t/lib/python3.13t/site-packages/pluggy/_callers.py:53: in run_old_style_hookwrapper
return result.get_result()
/Users/goldbaum/.pyenv/versions/3.13.5t/lib/python3.13t/site-packages/pluggy/_callers.py:38: in run_old_style_hookwrapper
res = yield
/Users/goldbaum/.pyenv/versions/3.13.5t/lib/python3.13t/site-packages/pluggy/_callers.py:53: in run_old_style_hookwrapper
return result.get_result()
/Users/goldbaum/.pyenv/versions/3.13.5t/lib/python3.13t/site-packages/pluggy/_callers.py:38: in run_old_style_hookwrapper
res = yield
/Users/goldbaum/.pyenv/versions/3.13.5t/lib/python3.13t/site-packages/pluggy/_callers.py:53: in run_old_style_hookwrapper
return result.get_result()
/Users/goldbaum/.pyenv/versions/3.13.5t/lib/python3.13t/site-packages/pluggy/_callers.py:38: in run_old_style_hookwrapper
res = yield
/Users/goldbaum/.pyenv/versions/3.13.5t/lib/python3.13t/site-packages/_pytest/runner.py:157: in pytest_runtest_setup
item.session._setupstate.setup(item)
/Users/goldbaum/.pyenv/versions/3.13.5t/lib/python3.13t/site-packages/_pytest/runner.py:497: in setup
raise exc
/Users/goldbaum/.pyenv/versions/3.13.5t/lib/python3.13t/site-packages/_pytest/runner.py:494: in setup
col.setup()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <TestCaseFunction test_noop>
def setup(self) -> None:
# A bound method to be called during teardown() if set (see 'runtest()').
self._explicit_tearDown: Optional[Callable[[], None]] = None
assert self.parent is not None
> self._testcase = self.parent.obj(self.name) # type: ignore[attr-defined]
E TypeError: Can't instantiate abstract class InvalidTestCase without an implementation for abstract method 'get_application'
/Users/goldbaum/.pyenv/versions/3.13.5t/lib/python3.13t/site-packages/_pytest/unittest.py:201: TypeError
========================================= short test summary info ==========================================
ERROR test_testcase_no_app.py::InvalidTestCase::test_noop - TypeError: Can't instantiate abstract class I...
============================================= 1 error in 0.18s =============================================
________________________________ test_response_with_bodypart_named[pyloop] _________________________________
[gw3] darwin -- Python 3.13.5 /Users/goldbaum/.pyenv/versions/3.13.5t/bin/python3.13
aiohttp_client = <function aiohttp_client.<locals>.go at 0x52e2d99b360>
tmp_path = PosixPath('/private/var/folders/nk/yds4mlh97kg9qdq745g715rw0000gn/T/pytest-of-goldbaum/pytest-1/popen-gw3/test_response_with_bodypart_na0')
async def test_response_with_bodypart_named(
aiohttp_client: AiohttpClient, tmp_path: pathlib.Path
) -> None:
async def handler(request: web.Request) -> web.Response:
reader = await request.multipart()
part = await reader.next()
return web.Response(body=part)
app = web.Application(client_max_size=2)
app.router.add_post("/", handler)
client = await aiohttp_client(app)
f = tmp_path / "foobar.txt"
f.write_text("test", encoding="utf8")
> with f.open("rb") as fd:
E ValueError: I/O operation on closed file.
aiohttp_client = <function aiohttp_client.<locals>.go at 0x52e2d99b360>
app = <Application 0x52e28d7e280>
body = b'test'
client = <aiohttp.test_utils.TestClient object at 0x52e29281430>
data = {'file': <_io.BufferedReader name='/private/var/folders/nk/yds4mlh97kg9qdq745g715rw0000gn/T/pytest-of-goldbaum/pytest-1/popen-gw3/test_response_with_bodypart_na0/foobar.txt'>}
f = PosixPath('/private/var/folders/nk/yds4mlh97kg9qdq745g715rw0000gn/T/pytest-of-goldbaum/pytest-1/popen-gw3/test_response_with_bodypart_na0/foobar.txt')
fd = <_io.BufferedReader name='/private/var/folders/nk/yds4mlh97kg9qdq745g715rw0000gn/T/pytest-of-goldbaum/pytest-1/popen-gw3/test_response_with_bodypart_na0/foobar.txt'>
handler = <function test_response_with_bodypart_named.<locals>.handler at 0x52e2d99c120>
resp = <ClientResponse(http://127.0.0.1:55063/) [200 OK]>
<CIMultiDictProxy('Content-Type': 'application/octet-stream', 'Cont...Transfer-Encoding': 'chunked', 'Date': 'Thu, 17 Jul 2025 16:02:44 GMT', 'Server': 'Python/3.13 aiohttp/4.0.0a2.dev0')>
tmp_path = PosixPath('/private/var/folders/nk/yds4mlh97kg9qdq745g715rw0000gn/T/pytest-of-goldbaum/pytest-1/popen-gw3/test_response_with_bodypart_na0')
tests/test_web_functional.py:1833: ValueError
I'm not sure if the maintainers are open to adding complexity to the test setup to avoid issues with test dependencies on the free-threaded build. If not, we probably need to wait until CFFI is unblocked at least.
[XPASS(strict)] https://github.com/pytest-dev/pytest/issues/13546
You're running a much older version of pytest.
with f.open("rb") as fd:E ValueError: I/O operation on closed file.
This one looks weird. How can it be a closed file when we're attempting to open it?
You're running a much older version of pytest.
Thanks! This is because of the hacked-together, manually constructed environment.
This one looks weird. How can it be a closed file when we're attempting to open it?
On a second look, I agree. Maybe there's something tricky going on in the pathlib implementation along the lines of https://github.com/python/cpython/issues/136248, which seems similar. I'll try to reproduce this outside of aiohttp.
I just re-did this exercise with a Python 3.14t interpreter. This required even more hacking of test dependencies, since a few of the dependencies in the test requirements file are pinned to versions that don't support 3.14.
With 3.14t, I no longer see the pathlib error I saw before. It's possible the fix for the issue I saw has been fixed in CPython main for a while and the fix was never backported to 3.13 - I'll try to pin that down so we can make sure there's a backport.
With 3.14t I see the following test failures and errors in the aiohttp tests:
ERROR tests/test_proxy_functional.py::test_uvloop_secure_https_proxy - AttributeError: 'NoneType' object has no attribute 'new_event_loop'
FAILED tests/test_helpers.py::test_when_timeout_smaller_second[pyloop] - assert 0.005357708083465695 == 0 ± 0.001
FAILED tests/test_cookiejar.py::test_pickle_format - AssertionError: assert defaultdict(<...'fifteenth'>}) == defaultdict(<...'fifteenth'>})
FAILED tests/test_circular_imports.py::test_no_warnings[aiohttp._http_parser] - subprocess.CalledProcessError: Command '('/Users/goldbaum/.pyenv/versions/3.14.0rc1t/bin/python3.14', '-W', 'error', '-W',...
FAILED tests/test_circular_imports.py::test_no_warnings[aiohttp._http_writer] - subprocess.CalledProcessError: Command '('/Users/goldbaum/.pyenv/versions/3.14.0rc1t/bin/python3.14', '-W', 'error', '-W',...
FAILED tests/test_circular_imports.py::test_no_warnings[aiohttp._websocket.mask] - subprocess.CalledProcessError: Command '('/Users/goldbaum/.pyenv/versions/3.14.0rc1t/bin/python3.14', '-W', 'error', '-W',...
I think these are probably all 3.14 compatibility issues? I would need to look closer.
Possibly. With 3.14 in the release candidate stage, we should be able to start preparing 3.14 releases now.
I've added 3.14 back to the testing matrix. Looks like initial blockers are isal and pydantic-core. I think pydantic is already resolved, but Dependabot seems to think it's not upgradeable, so may need a manual version bump.
When I tried yesterday the incompatible constraint on pydantic-core was coming in from python-on-whales
I've added 3.14 back to the testing matrix. Looks like initial blockers are isal and pydantic-core. I think pydantic is already resolved, but Dependabot seems to think it's not upgradeable, so may need a manual version bump.
I have updated my PR at https://github.com/aio-libs/aiohttp/pull/10872 to avoid installing isal and python-on-whales on 3.14 as they are test are only dependencies and skipped tests if not present, with that most tests are now passing except one in cookie jar which was changed in 3.14.
I think this is a good starting point to merge that than waiting for all such non-required dependencies to ship 3.14 wheels.
cc @Dreamsorcerer