Repository navigation
Support for free-threaded Python #118
Copy link
Copy link
Open
Description
Installing from source on Python 3.13 (M1 Mac) works fine:
$ python -VV
Python 3.13.0 (main, Oct 16 2024, 08:05:40) [Clang 18.1.8 ]
$ uv pip install "httptools==0.6.4" --no-binary :all:
Resolved 1 package in 1.31s
Built httptools==0.6.4
Prepared 1 package in 5.47s
Installed 1 package in 8ms
+ httptools==0.6.4
But in free-threaded Python:
$ python -VV
Python 3.13.0 experimental free-threading build (main, Oct 16 2024, 08:24:33) [Clang 18.1.8 ]
$ uv pip install "httptools==0.6.4" --no-binary :all:
Resolved 1 package in 1.14s
error: Failed to prepare distributions
Caused by: Failed to download and build `httptools==0.6.4`
Caused by: Build backend failed to build wheel through `build_wheel` (exit status: 1)
[stdout]
running bdist_wheel
running build
running build_py
copying httptools/_version.py -> build/lib.macosx-11.0-arm64-cpython-313t/httptools
copying httptools/__init__.py -> build/lib.macosx-11.0-arm64-cpython-313t/httptools
copying httptools/parser/__init__.py -> build/lib.macosx-11.0-arm64-cpython-313t/httptools/parser
copying httptools/parser/errors.py -> build/lib.macosx-11.0-arm64-cpython-313t/httptools/parser
running egg_info
writing httptools.egg-info/PKG-INFO
writing dependency_links to httptools.egg-info/dependency_links.txt
writing requirements to httptools.egg-info/requires.txt
writing top-level names to httptools.egg-info/top_level.txt
reading manifest file 'httptools.egg-info/SOURCES.txt'
reading manifest template 'MANIFEST.in'
adding license file 'LICENSE'
writing manifest file 'httptools.egg-info/SOURCES.txt'
copying httptools/parser/cparser.pxd -> build/lib.macosx-11.0-arm64-cpython-313t/httptools/parser
copying httptools/parser/parser.pyx -> build/lib.macosx-11.0-arm64-cpython-313t/httptools/parser
copying httptools/parser/python.pxd -> build/lib.macosx-11.0-arm64-cpython-313t/httptools/parser
copying httptools/parser/url_cparser.pxd -> build/lib.macosx-11.0-arm64-cpython-313t/httptools/parser
copying httptools/parser/url_parser.pyx -> build/lib.macosx-11.0-arm64-cpython-313t/httptools/parser
running build_ext
building 'httptools.parser.parser' extension
clang -fno-strict-overflow -Wsign-compare -Wunreachable-code -DNDEBUG -g -O3 -Wall -arch arm64 -mmacosx-version-min=11.0 -Wno-nullability-completeness -Wno-expansion-to-defined -Wno-undef-prefix -fPIC -Werror=unguarded-availability-new -I/Users/juan_cano/.cache/uv/sdists-v5/index/d2e01f6a682f0b5c/httptools/0.6.4/uI2NjLIM8YFjwSjCP_QiN/httptools-0.6.4.tar.gz/vendor/llhttp/include -I/Users/juan_cano/.cache/uv/sdists-v5/index/d2e01f6a682f0b5c/httptools/0.6.4/uI2NjLIM8YFjwSjCP_QiN/httptools-0.6.4.tar.gz/vendor/llhttp/src -I/Users/juan_cano/.cache/uv/builds-v0/.tmpAVsfvs/include -I/Users/juan_cano/.local/share/uv/python/cpython-3.13.0+freethreaded-macos-aarch64-none/include/python3.13t -c httptools/parser/parser.c -o build/temp.macosx-11.0-arm64-cpython-313t/httptools/parser/parser.o -O2
[stderr]
/Users/juan_cano/.cache/uv/builds-v0/.tmpAVsfvs/lib/python3.13t/site-packages/setuptools/_distutils/dist.py:261: UserWarning: Unknown distribution option: 'test_suite'
warnings.warn(msg)
httptools/parser/parser.c:2189:80: error: unknown type name '__pyx_vectorcallfunc'; did you mean 'vectorcallfunc'?
2189 | static CYTHON_INLINE PyObject *__Pyx_PyVectorcall_FastCallDict(PyObject *func, __pyx_vectorcallfunc vc, PyObject *const *args, size_t nargs, PyObject *kw);
| ^~~~~~~~~~~~~~~~~~~~
| vectorcallfunc
/Users/juan_cano/.local/share/uv/python/cpython-3.13.0+freethreaded-macos-aarch64-none/include/python3.13t/object.h:495:21: note: 'vectorcallfunc' declared here
495 | typedef PyObject *(*vectorcallfunc)(PyObject *callable, PyObject *const *args,
| ^
httptools/parser/parser.c:14183:69: error: unknown type name '__pyx_vectorcallfunc'; did you mean 'vectorcallfunc'?
14183 | static PyObject *__Pyx_PyVectorcall_FastCallDict_kw(PyObject *func, __pyx_vectorcallfunc vc, PyObject *const *args, size_t nargs, PyObject *kw)
| ^~~~~~~~~~~~~~~~~~~~
| vectorcallfunc
/Users/juan_cano/.local/share/uv/python/cpython-3.13.0+freethreaded-macos-aarch64-none/include/python3.13t/object.h:495:21: note: 'vectorcallfunc' declared here
495 | typedef PyObject *(*vectorcallfunc)(PyObject *callable, PyObject *const *args,
| ^
httptools/parser/parser.c:14228:80: error: unknown type name '__pyx_vectorcallfunc'; did you mean 'vectorcallfunc'?
14228 | static CYTHON_INLINE PyObject *__Pyx_PyVectorcall_FastCallDict(PyObject *func, __pyx_vectorcallfunc vc, PyObject *const *args, size_t nargs, PyObject *kw)
| ^~~~~~~~~~~~~~~~~~~~
| vectorcallfunc
/Users/juan_cano/.local/share/uv/python/cpython-3.13.0+freethreaded-macos-aarch64-none/include/python3.13t/object.h:495:21: note: 'vectorcallfunc' declared here
495 | typedef PyObject *(*vectorcallfunc)(PyObject *callable, PyObject *const *args,
| ^
httptools/parser/parser.c:14917:6: error: unknown type name '__pyx_vectorcallfunc'; did you mean 'vectorcallfunc'?
14917 | __pyx_vectorcallfunc vc = __Pyx_CyFunction_func_vectorcall(cyfunc);
| ^~~~~~~~~~~~~~~~~~~~
| vectorcallfunc
/Users/juan_cano/.local/share/uv/python/cpython-3.13.0+freethreaded-macos-aarch64-none/include/python3.13t/object.h:495:21: note: 'vectorcallfunc' declared here
495 | typedef PyObject *(*vectorcallfunc)(PyObject *callable, PyObject *const *args,
| ^
4 errors generated.
error: command '/usr/bin/clang' failed with exit code 1
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
Activity
ngoldbaum commentedon May 20, 2025
Cython 3.1 is out now, and httptools builds on the free-threaded build:
I'm happy to help out if anyone has any questions about adding support for free-threading. Also take a look at https://cython.readthedocs.io/en/latest/src/userguide/freethreading.html and https://py-free-threading.github.io.
xygwfxu commentedon Aug 7, 2025
@ngoldbaum
Since you have compiled httptools in your local repo, why would you not make a pull to this repo so we can see what you have done for compiling it in nogil?
I would like to see what you changed and checkout locally to use it.
Or maybe I could download your patch/branch from somewhere?
Thanks.
ngoldbaum commentedon Aug 7, 2025
@xygwfxu I just did
pip install .in the root of the repo:No patches or pull request necessary, at least to get it to build.
ngoldbaum commentedon Aug 7, 2025
I think the sdist includes bundled cython source code that was generated with an old version of Cython? Fixing that would require a new release with rebuilt cython sources.
ngoldbaum commentedon Sep 25, 2025
I opened #130 to modernize the packaging and fix CI. After that is merged I'll send in a followup to setup Python 3.14 and free-threaded support.
ngoldbaum commentedon Oct 14, 2025
This is done now and httptools 0.7.1 now builds successfully on the free-threaded build. All that's left is adding support for the free-threaded build in the package.
ngoldbaum commentedon Oct 20, 2025
TSan testing using
unittest-ftI just set up a test environment using a free-threaded 3.14t interpreter that I compiled with TSan instrumentation. Read here for more about using TSan to validate thread safety of Python C extensions. After installing the test dependencies following the CI job, I additionally installed
unittest-ft- a test runner forunittest-style tests that can run tests in a thread pool in parallel. I turned on the stress-testing mode and additionally randomized the test order. I also force-disabled the GIL from being re-enabled at runtime and set the TSan option to crash the interpreter if it detects an issue:On my ARM64 M3 Mac, I see no reports from TSan during the test run.
That validates that there isn't any global state that is accessible by running the current test suite in a thread pool. That's a good sign but not necessarily sufficient to declare the library to be thread-safe.
Safety of sharing
HttpParserinstances.Looking at the source code, It looks like the
HttpParserclass does have some state that mutates after__init__finishes - the_last_error,_current_header_name,_current_header_valueattributes. There might be more mutable state I'm missing.I think the
url_parseris thread-safe. There are probably ways to intentionally break it if someone passes a file to parse and mutates it simultaneously, but IMO that's user error.I'd appreciate it if the maintainers could let me know if they want to support sharing
HttpParserinstances between threads.Next steps
IMO it's a valid choice to document that sharing
HttpParserinstances isn't supported. You could leave things as-is and allow for the possibility of crashes if users do unsupported things.If you're willing to pay some cost to enforce thread safety, you can have a pattern where if two threads try to simultaneously read and mutate state, then users see an exception. You can implement that via an RWLock using non-blocking locking APIs. If a thread ever fails to acquire a read or write lock because another thread has a write lock, the acquiring thread raises an exception. That approach forces users to add synchronization if they really do want to share parsers, without forcing every user to pay that cost. Acquiring a locked mutex is very cheap and failing to acquire an unlocked mutex is also very cheap. The big downside is maintainability - you need to make sure the locking remains correct as the internals change.
Another approach is to use critical sections. The big downside there is you need to carefully audit and test to make sure the critical section doesn't get implicitly released by a call into the Python C API.
I don't see any existing parallel processing tests. I could add some, but I'd also appreciate some feedback on the issues I raised above. I'd really appreciate a specific suggestion for how to write parallel tests or some indication you'd prefer I not do that.
ngoldbaum commentedon Oct 20, 2025
I happened upon the talk from @da-woods at this year's EuroPython today: https://www.youtube.com/watch?v=7azKz3YP7eA. There's lots of content in that talk for thinking about the thread safety of Cython code.
ngoldbaum commentedon Oct 31, 2025
I'd still appreciate some feedback but barring that I think I'll open a followup PR that enables support for the free-threaded build and documents that sharing HTTPParser instances between threads isn't supported and suggests creating local per-thread HTTPParser instances.
uwu-420 commentedon Nov 26, 2025
@ngoldbaum Is it intended to make HTTPParser instances thread-safe eventually? Just from my own impression, but I think plenty of people are eager to use free-threaded Python by default for all their projects and making HTTPParser thread-safe would be a sensible thing to do imo.
ngoldbaum commentedon Nov 26, 2025
That's up to the maintainers to decide. IMO it's worth doing but there are performance, complexity, and maintainability trade-offs to consider.
fantix commentedon Nov 26, 2025
@ngoldbaum thank you for the very detailed follow-up and sorry for the delay!
Regarding the thread-safety of httptools, I'd like to know more about the use case of sharing a WIP parser between threads, like one thread A feeding the parser with I/O data, while the other thread B tries to read shared state from it? Note that the registered callbacks are invoked within thread A in a blocking manner, while I suppose the public parser APIs are usually meaningful only when invoked from such callbacks?
I'm not against adding synchronization primitives; it's just that - like you said - preventing concurrent access is just simpler (hence safer) to maintain if the thread-safety use case is not strong enough.
ngoldbaum commentedon Dec 2, 2025
I was more thinking along the lines of adding critical sections in the Cython implementation. Critical sections are no-ops on the GIL-enabled build but use locks on the free-threaded build. They give you similar guarantees compared with what the GIL used to provide.
Our team has recently been working with the explosion AI team on the spaCy dependency tree and it's a lot of similar
work. See explosion/cymem#53 and explosion/preshed#54.
The goal is to provide locking where the GIL used to and to prevent crashes and races that were impossible before.
kumaraditya303 commentedon Mar 23, 2026
I think given the current unclear use-case of sharing of parser across threads, we should perhaps only consider the usual case of a parser which is not shared, I don't think sharing a parser across threads is meaningful even in gil-enabled builds. For this I followed, what I had done for uvloop which has similar thread safety guarantee of "event loop should not be shared across threads" and created #139 to mark the cython extension as ft-compatible as first step.