Skip to content

Support for free-threaded Python #118

Open

Description

@astrojuanlu

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

Activity

  1. ngoldbaum commented on May 20, 2025

    @ngoldbaum
    Contributor

    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.

  2. xygwfxu commented on Aug 7, 2025

    @xygwfxu

    @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.

  3. ngoldbaum commented on Aug 7, 2025

    @ngoldbaum
    Contributor

    @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.

  4. ngoldbaum commented on Aug 7, 2025

    @ngoldbaum
    Contributor

    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.

  5. ngoldbaum commented on Sep 25, 2025

    @ngoldbaum
    Contributor

    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.

  6. ngoldbaum commented on Oct 14, 2025

    @ngoldbaum
    Contributor

    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.

  7. ngoldbaum commented on Oct 20, 2025

    @ngoldbaum
    Contributor

    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.

  8. ngoldbaum commented on Oct 20, 2025

    @ngoldbaum
    Contributor

    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.

  9. ngoldbaum commented on Oct 31, 2025

    @ngoldbaum
    Contributor

    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.

  10. uwu-420 commented on Nov 26, 2025

    @uwu-420

    @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.

  11. ngoldbaum commented on Nov 26, 2025

    @ngoldbaum
    Contributor

    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.

  12. fantix commented on Nov 26, 2025

    @fantix
    Member

    @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.

  13. ngoldbaum commented on Dec 2, 2025

    @ngoldbaum
    Contributor

    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.

  14. kumaraditya303 commented on Mar 23, 2026

    @kumaraditya303
    Contributor

    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 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions