Skip to main content

Free-threaded Python on GitHub Actions

View of the spinning room of a wool cloth manufacturing factory on Broad Street in Philadelphia, late nineteenth-century. Shows two women operating machines that wind bobbins with woolen thread for weaving.

GitHub Actions now supports experimental free-threaded CPython!

There are three ways to add it to your test matrix:

  • actions/setup-python: t suffix
  • actions/setup-uv: t suffix
  • actions/setup-python: freethreaded variable

actions/setup-python: t suffix #

Using actions/setup-python, you can add the t suffix for Python versions 3.13 and higher: 3.13t and 3.14t.

This is my preferred method, we can clearly see which versions are free-threaded and it’s straightforward to test both regular and free-threaded builds.

on: [push, pull_request, workflow_dispatch]

jobs:
  build:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        python-version: [
            "3.13",
            "3.13t", # add this!
            "3.14",
            "3.14t", # add this!
          ]
        os: ["windows-latest", "macos-latest", "ubuntu-latest"]

    steps:
      - uses: actions/checkout@v4

      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
          allow-prereleases: true # needed for 3.14

      - run: |
          python --version --version
          python -c "import sys; print('sys._is_gil_enabled:', sys._is_gil_enabled())"
          python -c "import sysconfig; print('Py_GIL_DISABLED:', sysconfig.get_config_var('Py_GIL_DISABLED'))"

Regular builds will output something like:

Python 3.14.0a6 (main, Mar 17 2025, 02:44:29) [GCC 13.3.0]
sys._is_gil_enabled: True
Py_GIL_DISABLED: 0

And free-threaded builds will output something like:

Python 3.14.0a6 experimental free-threading build (main, Mar 17 2025, 02:44:30) [GCC 13.3.0]
sys._is_gil_enabled: False
Py_GIL_DISABLED: 1

For example: hugovk/test/actions/runs/14057185035

actions/setup-uv: t suffix #

Similarly, you can install uv with astral/setup-uv and use that to set up free-threaded Python using the t suffix.

on: [push, pull_request, workflow_dispatch]

jobs:
  build:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        python-version: [
            "3.13",
            "3.13t", # add this!
            "3.14",
            "3.14t", # add this!
          ]
        os: ["windows-latest", "macos-latest", "ubuntu-latest"]

    steps:
      - uses: actions/checkout@v4

      - name: Set up Python ${{ matrix.python-version }}
        uses: astral-sh/setup-uv@v5 # change this!
        with:
          python-version: ${{ matrix.python-version }}
          enable-cache: false # only needed for this example with no dependencies

      - run: |
          python --version --version
          python -c "import sys; print('sys._is_gil_enabled:', sys._is_gil_enabled())"
          python -c "import sysconfig; print('Py_GIL_DISABLED:', sysconfig.get_config_var('Py_GIL_DISABLED'))"

For example: hugovk/test/actions/runs/13967959519

actions/setup-python: freethreaded variable #

Back to actions/setup-python, you can also set the freethreaded variable for 3.13 and higher.

on: [push, pull_request, workflow_dispatch]

jobs:
  build:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        python-version: ["3.13", "3.14"]
        os: ["windows-latest", "macos-latest", "ubuntu-latest"]

    steps:
      - uses: actions/checkout@v4

      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
          allow-prereleases: true # needed for 3.14
          freethreaded: true # add this!

      - run: |
          python --version --version
          python -c "import sys; print('sys._is_gil_enabled:', sys._is_gil_enabled())"
          python -c "import sysconfig; print('Py_GIL_DISABLED:', sysconfig.get_config_var('Py_GIL_DISABLED'))"

For example: hugovk/test/actions/runs/39359291708

PYTHON_GIL=0 #

And you may want to set PYTHON_GIL=0 to force Python to keep the GIL disabled, even after importing a module that doesn’t support running without it.

See Running Python with the GIL Disabled for more info.

With the t suffix:

- name: Set PYTHON_GIL
  if: endsWith(matrix.python-version, 't')
  run: |
    echo "PYTHON_GIL=0" >> "$GITHUB_ENV"

With the freethreaded variable:

- name: Set PYTHON_GIL
  if: "${{ matrix.freethreaded }}"
  run: |
    echo "PYTHON_GIL=0" >> "$GITHUB_ENV"

Please test! #

For free-threaded Python to succeed and become the default, it’s essential there is ecosystem and community support. Library maintainers: please test it and where needed, adapt your code, and publish free-threaded wheels so others can test their code that depends on yours. Everyone else: please test your code too!

See also #


Header photo: “Spinning Room, Winding Bobbins with Woolen Yarn for Weaving, Philadelphia, PA” by Library Company of Philadelphia, with no known copyright restrictions.