Support for free-threaded Python
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
Cython 3.1 is out now, and httptools builds on the free-threaded build:
goldbaum at Nathans-MacBook-Pro in ~/Documents/httptools on master
± pip install .
Processing /Users/goldbaum/Documents/httptools
Preparing metadata (setup.py) ... done
Building wheels for collected packages: httptools
Building wheel for httptools (setup.py) ... done
Created wheel for httptools: filename=httptools-0.7.0.dev0-cp313-cp313t-macosx_15_0_arm64.whl size=109184 sha256=0c40984cc0b4ade3ddafa4e1951bf302b3dcad62bbb9264f702df9c7e1d3fb3e
Stored in directory: /private/var/folders/nk/yds4mlh97kg9qdq745g715rw0000gn/T/pip-ephem-wheel-cache-eiyb5ne8/wheels/ea/f5/f8/2339ab0e4c1eb68b3e46381d04b922d0997650e9cdb84d0699
Successfully built httptools
Installing collected packages: httptools
Successfully installed httptools-0.7.0.dev0
[notice] A new release of pip is available: 25.0.1 -> 25.1.1
[notice] To update, run: pip install --upgrade pip
goldbaum at Nathans-MacBook-Pro in ~/Documents/httptools on master
± python -VV
Python 3.13.3 experimental free-threading build (main, May 14 2025, 08:53:19) [Clang 17.0.0 (clang-1700.0.13.3)]
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.
@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.
@xygwfxu I just did pip install . in the root of the repo:
goldbaum at Nathans-MBP in ~/Documents/httptools on master
± pip install .
Processing /Users/goldbaum/Documents/httptools
Preparing metadata (setup.py) ... done
Building wheels for collected packages: httptools
DEPRECATION: Building 'httptools' using the legacy setup.py bdist_wheel mechanism, which will be removed in a future version. pip 25.3 will enforce this behaviour change. A possible replacement is to use the standardized build interface by setting the `--use-pep517` option, (possibly combined with `--no-build-isolation`), or adding a `pyproject.toml` file to the source tree of 'httptools'. Discussion can be found at https://github.com/pypa/pip/issues/6334
Building wheel for httptools (setup.py) ... done
Created wheel for httptools: filename=httptools-0.7.0.dev0-cp313-cp313t-macosx_15_0_arm64.whl size=109188 sha256=e738526aec37e81490661576aa71e0a83526754ecf46d36062c062898a237d89
Stored in directory: /private/var/folders/nk/yds4mlh97kg9qdq745g715rw0000gn/T/pip-ephem-wheel-cache-jxv1_54v/wheels/ea/f5/f8/2339ab0e4c1eb68b3e46381d04b922d0997650e9cdb84d0699
Successfully built httptools
Installing collected packages: httptools
Successfully installed httptools-0.7.0.dev0
[notice] A new release of pip is available: 25.1.1 -> 25.2
[notice] To update, run: pip install --upgrade pip
goldbaum at Nathans-MBP in ~/Documents/httptools on master
± git diff
No patches or pull request necessary, at least to get it to build.
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.
I opened https://github.com/MagicStack/httptools/pull/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.
After that is merged I'll send in a followup to setup Python 3.14 and free-threaded support.
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.
TSan testing using unittest-ft
I 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 for unittest-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:
TSAN_OPTIONS=halt_on_error=1 PYTHON_GIL=0 unittest-ft -v -s -r
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 HttpParser instances.
Looking at the source code, It looks like the HttpParser class does have some state that mutates after __init__ finishes - the _last_error, _current_header_name, _current_header_value attributes. There might be more mutable state I'm missing.
I think the url_parser is 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 HttpParser instances between threads.
Next steps
IMO it's a valid choice to document that sharing HttpParser instances 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.
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.
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.
@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.
Is it intended to make HTTPParser instances thread-safe eventually
That's up to the maintainers to decide. IMO it's worth doing but there are performance, complexity, and maintainability trade-offs to consider.
@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.