feat(toolchain): support freethreaded toolchains (#2372)

Before this PR freethreaded toolchains were not possible to be used,
this adds the minimum plumbing to get the things working. Coverage
support is also added.

Whilst at it:
- Add plumbing to print checksums only for a particular python version.
- Bump the remaining toolchain versions that used to use the 20241008
release
- Pass around the loaded platform list so that we are only defining
toolchains for the platforms that we have loaded the hermetic toolchain
for.

Tested:
```
$ bazel run --//python/config_settings:python_version=3.13.0 --//python/config_settings:py_freethreaded="yes" //python/private:current_interpreter_executable
...
Python 3.13.0 experimental free-threading build (main, Oct 16 2024, 03:26:14) [Clang 18.1.8 ] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>
```

Closes #2129.
Work towards #2386.
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 38b9161..65389db 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -16,6 +16,10 @@
 # See https://pre-commit.com for more information
 # See https://pre-commit.com/hooks.html for more hooks
 repos:
+  - repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v5.0.0  # Use the ref you want to point at
+    hooks:
+      - id: check-merge-conflict
   - repo: https://github.com/keith/pre-commit-buildifier
     rev: 6.1.0
     hooks:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 93abc5b..f3b2465 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -28,6 +28,14 @@
 {#v0-0-0-changed}
 ### Changed
 * (deps) bazel_skylib 1.6.1 -> 1.7.1
+* (toolchains) Use the latest indygreg toolchain release [20241016] for Python versions:
+    * 3.9.20
+    * 3.10.15
+    * 3.11.10
+    * 3.12.7
+    * 3.13.0
+
+[20241016]: https://github.com/indygreg/python-build-standalone/releases/tag/20241016
 
 {#v0-0-0-fixed}
 ### Fixed
@@ -35,7 +43,9 @@
 
 {#v0-0-0-added}
 ### Added
-* Nothing yet
+* (toolchain) Support for freethreaded Python toolchains is now available. Use
+  the config flag `//python/config_settings:py_freethreaded` to toggle the
+  selection of the free-threaded toolchains.
 
 {#v0-0-0-removed}
 ### Removed
diff --git a/docs/api/rules_python/python/config_settings/index.md b/docs/api/rules_python/python/config_settings/index.md
index 7c7421b..ef829ba 100644
--- a/docs/api/rules_python/python/config_settings/index.md
+++ b/docs/api/rules_python/python/config_settings/index.md
@@ -149,6 +149,16 @@
 :::
 ::::
 
+::::{bzl:flag} py_freethreaded
+Set whether to use an interpreter with the experimental freethreaded option set to true.
+
+Values:
+* `no`: Use regular Python toolchains, default.
+* `yes`: Use the experimental Python toolchain with freethreaded compile option enabled.
+:::{versionadded} 0.38.0
+:::
+::::
+
 ::::{bzl:flag} pip_whl
 Set what distributions are used in the `pip` integration.
 
diff --git a/python/config_settings/BUILD.bazel b/python/config_settings/BUILD.bazel
index c530afe..6d34ee9 100644
--- a/python/config_settings/BUILD.bazel
+++ b/python/config_settings/BUILD.bazel
@@ -5,6 +5,7 @@
     "AddSrcsToRunfilesFlag",
     "BootstrapImplFlag",
     "ExecToolsToolchainFlag",
+    "FreeThreadedFlag",
     "PrecompileFlag",
     "PrecompileSourceRetentionFlag",
 )
@@ -92,6 +93,19 @@
     visibility = ["//visibility:public"],
 )
 
+string_flag(
+    name = "py_freethreaded",
+    build_setting_default = FreeThreadedFlag.NO,
+    values = sorted(FreeThreadedFlag.__members__.values()),
+    visibility = ["//visibility:public"],
+)
+
+config_setting(
+    name = "is_py_freethreaded",
+    flag_values = {":py_freethreaded": FreeThreadedFlag.YES},
+    visibility = ["//visibility:public"],
+)
+
 # pip.parse related flags
 
 string_flag(
diff --git a/python/private/BUILD.bazel b/python/private/BUILD.bazel
index bbee4de..1e972c5 100644
--- a/python/private/BUILD.bazel
+++ b/python/private/BUILD.bazel
@@ -256,6 +256,7 @@
     deps = [
         ":py_toolchain_suite_bzl",
         ":text_util_bzl",
+        "//python:versions_bzl",
     ],
 )
 
diff --git a/python/private/coverage_deps.bzl b/python/private/coverage_deps.bzl
index d3a6d96..e80e8ee 100644
--- a/python/private/coverage_deps.bzl
+++ b/python/private/coverage_deps.bzl
@@ -80,11 +80,23 @@
             "https://files.pythonhosted.org/packages/b9/67/e1413d5a8591622a46dd04ff80873b04c849268831ed5c304c16433e7e30/coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl",
             "a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9",
         ),
+        "aarch64-apple-darwin-freethreaded": (
+            "https://files.pythonhosted.org/packages/c4/ae/b5d58dff26cade02ada6ca612a76447acd69dccdbb3a478e9e088eb3d4b9/coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl",
+            "502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962",
+        ),
         "aarch64-unknown-linux-gnu": (
+            "https://files.pythonhosted.org/packages/14/5b/9dec847b305e44a5634d0fb8498d135ab1d88330482b74065fcec0622224/coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
+            "d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c",
+        ),
+        "aarch64-unknown-linux-gnu-freethreaded": (
             "https://files.pythonhosted.org/packages/b8/d7/62095e355ec0613b08dfb19206ce3033a0eedb6f4a67af5ed267a8800642/coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
             "6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb",
         ),
         "x86_64-unknown-linux-gnu": (
+            "https://files.pythonhosted.org/packages/f7/95/d2fd31f1d638df806cae59d7daea5abf2b15b5234016a5ebb502c2f3f7ee/coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
+            "78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060",
+        ),
+        "x86_64-unknown-linux-gnu-freethreaded": (
             "https://files.pythonhosted.org/packages/8b/61/a7a6a55dd266007ed3b1df7a3386a0d760d014542d72f7c2c6938483b7bd/coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
             "13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b",
         ),
diff --git a/python/private/flags.bzl b/python/private/flags.bzl
index c190cf6..5239771 100644
--- a/python/private/flags.bzl
+++ b/python/private/flags.bzl
@@ -122,3 +122,13 @@
     OMIT_SOURCE = "omit_source",
     get_effective_value = _precompile_source_retention_flag_get_effective_value,
 )
+
+# Used for matching freethreaded toolchains and would have to be used in wheels
+# as well.
+# buildifier: disable=name-conventions
+FreeThreadedFlag = enum(
+    # Use freethreaded python toolchain and wheels.
+    YES = "yes",
+    # Do not use freethreaded python toolchain and wheels.
+    NO = "no",
+)
diff --git a/python/private/hermetic_runtime_repo_setup.bzl b/python/private/hermetic_runtime_repo_setup.bzl
index cf9a5a6..3f7bb5d 100644
--- a/python/private/hermetic_runtime_repo_setup.bzl
+++ b/python/private/hermetic_runtime_repo_setup.bzl
@@ -21,6 +21,8 @@
 load(":py_exec_tools_toolchain.bzl", "py_exec_tools_toolchain")
 load(":semver.bzl", "semver")
 
+_IS_FREETHREADED = Label("//python/config_settings:is_py_freethreaded")
+
 def define_hermetic_runtime_toolchain_impl(
         *,
         name,
@@ -45,7 +47,7 @@
         python_version: {type}`str` The Python version, in `major.minor.micro`
             format.
         python_bin: {type}`str` The path to the Python binary within the
-            repositoroy.
+            repository.
         coverage_tool: {type}`str` optional target to the coverage tool to
             use.
     """
@@ -67,19 +69,23 @@
             exclude = [
                 # Unused shared libraries. `python` executable and the `:libpython` target
                 # depend on `libpython{python_version}.so.1.0`.
-                "lib/libpython{major}.{minor}.so".format(**version_dict),
+                "lib/libpython{major}.{minor}*.so".format(**version_dict),
                 # static libraries
                 "lib/**/*.a",
                 # tests for the standard libraries.
-                "lib/python{major}.{minor}/**/test/**".format(**version_dict),
-                "lib/python{major}.{minor}/**/tests/**".format(**version_dict),
-                "**/__pycache__/*.pyc.*",  # During pyc creation, temp files named *.pyc.NNN are created
+                "lib/python{major}.{minor}*/**/test/**".format(**version_dict),
+                "lib/python{major}.{minor}*/**/tests/**".format(**version_dict),
+                # During pyc creation, temp files named *.pyc.NNN are created
+                "**/__pycache__/*.pyc.*",
             ] + glob_excludes.version_dependent_exclusions() + extra_files_glob_exclude,
         ),
     )
     cc_import(
         name = "interface",
-        interface_library = "libs/python{major}{minor}.lib".format(**version_dict),
+        interface_library = select({
+            _IS_FREETHREADED: "libs/python{major}{minor}t.lib".format(**version_dict),
+            "//conditions:default": "libs/python{major}{minor}.lib".format(**version_dict),
+        }),
         system_provided = True,
     )
 
@@ -96,14 +102,62 @@
         hdrs = [":includes"],
         includes = [
             "include",
-            "include/python{major}.{minor}".format(**version_dict),
-            "include/python{major}.{minor}m".format(**version_dict),
-        ],
+        ] + select({
+            _IS_FREETHREADED: [
+                "include/python{major}.{minor}t".format(**version_dict),
+            ],
+            "//conditions:default": [
+                "include/python{major}.{minor}".format(**version_dict),
+                "include/python{major}.{minor}m".format(**version_dict),
+            ],
+        }),
     )
+    native.config_setting(
+        name = "is_freethreaded_linux",
+        flag_values = {
+            Label("//python/config_settings:py_freethreaded"): "yes",
+        },
+        constraint_values = [
+            "@platforms//os:linux",
+        ],
+        visibility = ["//visibility:private"],
+    )
+    native.config_setting(
+        name = "is_freethreaded_osx",
+        flag_values = {
+            Label("//python/config_settings:py_freethreaded"): "yes",
+        },
+        constraint_values = [
+            "@platforms//os:osx",
+        ],
+        visibility = ["//visibility:private"],
+    )
+    native.config_setting(
+        name = "is_freethreaded_windows",
+        flag_values = {
+            Label("//python/config_settings:py_freethreaded"): "yes",
+        },
+        constraint_values = [
+            "@platforms//os:windows",
+        ],
+        visibility = ["//visibility:private"],
+    )
+
     cc_library(
         name = "libpython",
         hdrs = [":includes"],
         srcs = select({
+            ":is_freethreaded_linux": [
+                "lib/libpython{major}.{minor}t.so".format(**version_dict),
+                "lib/libpython{major}.{minor}t.so.1.0".format(**version_dict),
+            ],
+            ":is_freethreaded_osx": [
+                "lib/libpython{major}.{minor}t.dylib".format(**version_dict),
+            ],
+            ":is_freethreaded_windows": [
+                "python3.dll",
+                "libs/python{major}{minor}t.lib".format(**version_dict),
+            ],
             "@platforms//os:linux": [
                 "lib/libpython{major}.{minor}.so".format(**version_dict),
                 "lib/libpython{major}.{minor}.so.1.0".format(**version_dict),
@@ -132,12 +186,18 @@
             "micro": str(version_info.patch),
             "minor": str(version_info.minor),
         },
-        # Convert empty string to None
-        coverage_tool = coverage_tool or None,
+        coverage_tool = select({
+            # Convert empty string to None
+            ":coverage_enabled": coverage_tool or None,
+            "//conditions:default": None,
+        }),
         python_version = "PY3",
         implementation_name = "cpython",
         # See https://peps.python.org/pep-3147/ for pyc tag infix format
-        pyc_tag = "cpython-{major}{minor}".format(**version_dict),
+        pyc_tag = select({
+            _IS_FREETHREADED: "cpython-{major}{minor}t".format(**version_dict),
+            "//conditions:default": "cpython-{major}{minor}".format(**version_dict),
+        }),
     )
 
     py_runtime_pair(
diff --git a/python/private/python.bzl b/python/private/python.bzl
index d2b1007..8632554 100644
--- a/python/private/python.bzl
+++ b/python/private/python.bzl
@@ -213,6 +213,7 @@
 def _python_impl(module_ctx):
     py = parse_modules(module_ctx = module_ctx)
 
+    loaded_platforms = {}
     for toolchain_info in py.toolchains:
         # Ensure that we pass the full version here.
         full_python_version = full_version(
@@ -228,7 +229,7 @@
         kwargs.update(py.config.kwargs.get(toolchain_info.python_version, {}))
         kwargs.update(py.config.kwargs.get(full_python_version, {}))
         kwargs.update(py.config.default)
-        python_register_toolchains(
+        loaded_platforms[full_python_version] = python_register_toolchains(
             name = toolchain_info.name,
             _internal_bzlmod_toolchain_call = True,
             **kwargs
@@ -257,6 +258,7 @@
             for i in range(len(py.toolchains))
         ],
         toolchain_user_repository_names = [t.name for t in py.toolchains],
+        loaded_platforms = loaded_platforms,
     )
 
     # This is require in order to support multiple version py_test
@@ -464,7 +466,7 @@
             "url": {
                 platform: [item["url"]]
                 for platform in item["sha256"]
-            },
+            } if type(item["url"]) == type("") else item["url"],
         }
         for version, item in TOOL_VERSIONS.items()
     }
diff --git a/python/private/python_register_toolchains.bzl b/python/private/python_register_toolchains.bzl
index 64b66d5..98c8e5b 100644
--- a/python/private/python_register_toolchains.bzl
+++ b/python/private/python_register_toolchains.bzl
@@ -73,6 +73,9 @@
         minor_mapping: {type}`dict[str, str]` contains a mapping from `X.Y` to `X.Y.Z`
             version.
         **kwargs: passed to each {obj}`python_repository` call.
+
+    Returns:
+        On bzlmod this returns the loaded platform labels. Otherwise None.
     """
     bzlmod_toolchain_call = kwargs.pop("_internal_bzlmod_toolchain_call", False)
     if bzlmod_toolchain_call:
@@ -168,11 +171,13 @@
 
     # in bzlmod we write out our own toolchain repos
     if bzlmod_toolchain_call:
-        return
+        return loaded_platforms
 
     toolchains_repo(
         name = toolchain_repo_name,
         python_version = python_version,
         set_python_version_constraint = set_python_version_constraint,
         user_repository_name = name,
+        platforms = loaded_platforms,
     )
+    return None
diff --git a/python/private/python_repository.bzl b/python/private/python_repository.bzl
index e44bdd1..9ffa196 100644
--- a/python/private/python_repository.bzl
+++ b/python/private/python_repository.bzl
@@ -15,7 +15,7 @@
 """This file contains repository rules and macros to support toolchain registration.
 """
 
-load("//python:versions.bzl", "PLATFORMS")
+load("//python:versions.bzl", "FREETHREADED", "PLATFORMS")
 load(":auth.bzl", "get_auth")
 load(":repo_utils.bzl", "REPO_DEBUG_ENV_VAR", "repo_utils")
 load(":text_util.bzl", "render")
@@ -63,8 +63,12 @@
     platform = rctx.attr.platform
     python_version = rctx.attr.python_version
     python_version_info = python_version.split(".")
-    python_short_version = "{0}.{1}".format(*python_version_info)
     release_filename = rctx.attr.release_filename
+    version_suffix = "t" if FREETHREADED in release_filename else ""
+    python_short_version = "{0}.{1}{suffix}".format(
+        suffix = version_suffix,
+        *python_version_info
+    )
     urls = rctx.attr.urls or [rctx.attr.url]
     auth = get_auth(rctx, urls)
 
diff --git a/python/private/pythons_hub.bzl b/python/private/pythons_hub.bzl
index fdaad60..8afee5a 100644
--- a/python/private/pythons_hub.bzl
+++ b/python/private/pythons_hub.bzl
@@ -14,6 +14,7 @@
 
 "Repo rule used by bzlmod extension to create a repo that has a map of Python interpreters and their labels"
 
+load("//python:versions.bzl", "PLATFORMS")
 load(":text_util.bzl", "render")
 load(":toolchains_repo.bzl", "python_toolchain_build_file_content")
 
@@ -46,7 +47,8 @@
         python_versions,
         set_python_version_constraints,
         user_repository_names,
-        workspace_location):
+        workspace_location,
+        loaded_platforms):
     """This macro iterates over each of the lists and returns the toolchain content.
 
     python_toolchain_build_file_content is called to generate each of the toolchain
@@ -65,6 +67,11 @@
                 python_version = python_versions[i],
                 set_python_version_constraint = set_python_version_constraints[i],
                 user_repository_name = user_repository_names[i],
+                loaded_platforms = {
+                    k: v
+                    for k, v in PLATFORMS.items()
+                    if k in loaded_platforms[python_versions[i]]
+                },
             )
             for i in range(len(python_versions))
         ],
@@ -103,6 +110,7 @@
             rctx.attr.toolchain_set_python_version_constraints,
             rctx.attr.toolchain_user_repository_names,
             rctx.attr._rules_python_workspace,
+            rctx.attr.loaded_platforms,
         ),
         executable = False,
     )
@@ -149,6 +157,9 @@
             doc = "Default Python version for the build in `X.Y` or `X.Y.Z` format.",
             mandatory = True,
         ),
+        "loaded_platforms": attr.string_list_dict(
+            doc = "The list of loaded platforms keyed by the toolchain full python version",
+        ),
         "minor_mapping": attr.string_dict(
             doc = "The minor mapping of the `X.Y` to `X.Y.Z` format that is used in config settings.",
             mandatory = True,
diff --git a/python/private/toolchains_repo.bzl b/python/private/toolchains_repo.bzl
index d21e46a..d21fb53 100644
--- a/python/private/toolchains_repo.bzl
+++ b/python/private/toolchains_repo.bzl
@@ -41,7 +41,8 @@
         prefix,
         python_version,
         set_python_version_constraint,
-        user_repository_name):
+        user_repository_name,
+        loaded_platforms):
     """Creates the content for toolchain definitions for a build file.
 
     Args:
@@ -51,6 +52,8 @@
             have the Python version constraint added as a requirement for
             matching the toolchain, "False" if not.
         user_repository_name: names for the user repos
+        loaded_platforms: {type}`struct` the list of platform structs defining the
+            loaded platforms. It is as they are defined in `//python:versions.bzl`.
 
     Returns:
         build_content: Text containing toolchain definitions
@@ -77,7 +80,7 @@
             prefix = prefix,
             python_version = python_version,
         )
-        for platform, meta in PLATFORMS.items()
+        for platform, meta in loaded_platforms.items()
     ])
 
 def _toolchains_repo_impl(rctx):
@@ -100,6 +103,11 @@
         python_version = rctx.attr.python_version,
         set_python_version_constraint = str(rctx.attr.set_python_version_constraint),
         user_repository_name = rctx.attr.user_repository_name,
+        loaded_platforms = {
+            k: v
+            for k, v in PLATFORMS.items()
+            if k in rctx.attr.platforms
+        },
     )
 
     rctx.file("BUILD.bazel", build_content + toolchains)
@@ -109,6 +117,7 @@
     doc = "Creates a repository with toolchain definitions for all known platforms " +
           "which can be registered or selected.",
     attrs = {
+        "platforms": attr.string_list(doc = "List of platforms for which the toolchain definitions shall be created"),
         "python_version": attr.string(doc = "The Python version."),
         "set_python_version_constraint": attr.bool(doc = "if target_compatible_with for the toolchain should set the version constraint"),
         "user_repository_name": attr.string(doc = "what the user chose for the base name"),
@@ -390,6 +399,9 @@
     """
     host_platform = None
     for platform, meta in PLATFORMS.items():
+        if "freethreaded" in platform:
+            continue
+
         if meta.os_name == os_name and meta.arch == arch:
             host_platform = platform
     if not host_platform:
diff --git a/python/versions.bzl b/python/versions.bzl
index ae017e3..774c24d 100644
--- a/python/versions.bzl
+++ b/python/versions.bzl
@@ -19,12 +19,13 @@
 MACOS_NAME = "mac os"
 LINUX_NAME = "linux"
 WINDOWS_NAME = "windows"
+FREETHREADED = "freethreaded"
 
 DEFAULT_RELEASE_BASE_URL = "https://github.com/indygreg/python-build-standalone/releases/download"
 
 # When updating the versions and releases, run the following command to get
 # the hashes:
-#   bazel run //python/private:print_toolchains_checksums
+#   bazel run //python/private:print_toolchains_checksums --//python/config_settings:python_version={major}.{minor}.{patch}
 #
 # Note, to users looking at how to specify their tool versions, coverage_tool version for each
 # interpreter can be specified by:
@@ -237,15 +238,15 @@
         "strip_prefix": "python",
     },
     "3.9.20": {
-        "url": "20241008/cpython-{python_version}+20241008-{platform}-{build}.tar.gz",
+        "url": "20241016/cpython-{python_version}+20241016-{platform}-{build}.tar.gz",
         "sha256": {
-            "aarch64-apple-darwin": "dde4c3662e8b4ea336af12b94e7963d4c9b4b847e6f4a5a2921d801fbc75d55c",
-            "aarch64-unknown-linux-gnu": "adb22acc4f5417ecb6113e4beb98f1a1492bcf631b3d3094135f60d1c6794e07",
-            "ppc64le-unknown-linux-gnu": "abc12738616d3d87e878cd022c4d6a3d7cb6c130a6f3859996ce758a90c8abae",
-            "s390x-unknown-linux-gnu": "bb037b3b266524df5a27f384755b2eab397837b3c955041145434261248a731d",
-            "x86_64-apple-darwin": "980fd160c8a3e7839d808055b9497e653bd7be94dcc9cae6db0ddcb343bc5ad6",
-            "x86_64-pc-windows-msvc": "dc12754f52b7cfcdded91c10953a96ed7d9b08eff54623ee5b819cec13f4715a",
-            "x86_64-unknown-linux-gnu": "ddae7e904f5ecdff4c8993eb5256fbcec1e477923b40ec0515ffc77706dc2951",
+            "aarch64-apple-darwin": "34ab2bc4c51502145e1a624b4e4ea06877e3d1934a88cc73ac2e0fd5fd439b75",
+            "aarch64-unknown-linux-gnu": "1e486c054a4e86666cf24e04f5e29456324ba9c2b95bf1cae1805be90d3da154",
+            "ppc64le-unknown-linux-gnu": "9a24ccdbfc7f67545d859128f02a3150a160ea6c2fc134b0773bf56f2d90b397",
+            "s390x-unknown-linux-gnu": "2cee381069bf344fb20eba609af92dfe7ba67eb75bea08eeccf11048a2c380c0",
+            "x86_64-apple-darwin": "193dc7f0284e4917d52b17a077924474882ee172872f2257cfe3375d6d468ed9",
+            "x86_64-pc-windows-msvc": "5069008a237b90f6f7a86956903f2a0221b90d471daa6e4a94831eaa399e3993",
+            "x86_64-unknown-linux-gnu": "c20ee831f7f46c58fa57919b75a40eb2b6a31e03fd29aaa4e8dab4b9c4b60d5d",
         },
         "strip_prefix": "python",
     },
@@ -356,15 +357,15 @@
         "strip_prefix": "python",
     },
     "3.10.15": {
-        "url": "20241008/cpython-{python_version}+20241008-{platform}-{build}.tar.gz",
+        "url": "20241016/cpython-{python_version}+20241016-{platform}-{build}.tar.gz",
         "sha256": {
-            "aarch64-apple-darwin": "6bfed646145b9f1f512bbf3c37de8a29fae3544559c501185f552c3b92dc270b",
-            "aarch64-unknown-linux-gnu": "51f08e2132dca177ac90175536118b3c01c106ec253b93db04e3ca7484525d00",
-            "ppc64le-unknown-linux-gnu": "44b05f1f831fbef00b36f5d6ef82f308e32d3dee58e1272d1fac26004ce7c76f",
-            "s390x-unknown-linux-gnu": "793bd6c565bd24b6db8e573d599492c6fddbaee43e4b4aeef240ada1105287d7",
-            "x86_64-apple-darwin": "df1324c960b9023cfebfd2716f69af57156d823a4d286d8e67ffc4f876309611",
-            "x86_64-pc-windows-msvc": "c519cb6bbb8caf508e3f3b91a3dd633b4bebdf84217ab34033a10c902b8a8519",
-            "x86_64-unknown-linux-gnu": "5e07b34c66fbd99f1e2f06d3d42aed04c0f2991e66c1d171fb43e04b7ae71ad5",
+            "aarch64-apple-darwin": "f64776f455a44c24d50f947c813738cfb7b9ac43732c44891bc831fa7940a33c",
+            "aarch64-unknown-linux-gnu": "eb58581f85fde83d1f3e8e1f8c6f5a15c7ae4fdbe3b1d1083931f9167fdd8dbc",
+            "ppc64le-unknown-linux-gnu": "0c45af4e7525e2db59901606db32b2896ac1e9830c6f95551402207f537c2ce4",
+            "s390x-unknown-linux-gnu": "de205896b070e6f5259ac0f2b3379eead875ea84e6a6ef533b89886fcbb46a4c",
+            "x86_64-apple-darwin": "90b46dfb1abd98d45663c7a2a8c45d3047a59391d8586d71b459cec7b75f662b",
+            "x86_64-pc-windows-msvc": "e48952619796c66ec9719867b87be97edca791c2ef7fbf87d42c417c3331609e",
+            "x86_64-unknown-linux-gnu": "3db2171e03c1a7acdc599fba583c1b92306d3788b375c9323077367af1e9d9de",
         },
         "strip_prefix": "python",
     },
@@ -470,15 +471,15 @@
         "strip_prefix": "python",
     },
     "3.11.10": {
-        "url": "20241008/cpython-{python_version}+20241008-{platform}-{build}.tar.gz",
+        "url": "20241016/cpython-{python_version}+20241016-{platform}-{build}.tar.gz",
         "sha256": {
-            "aarch64-apple-darwin": "ecdc9c042b8f97bff211fcf9425bc51c96acd4037df1565964e89816f2c9564d",
-            "aarch64-unknown-linux-gnu": "320635e957e13d2e10d70a3031563d032fae9e40e60e5ec32bc353643fae1335",
-            "ppc64le-unknown-linux-gnu": "7eed40dc5751046e2164b1a3f08f177c2c965064f1e3b0f84c00f3f715d099ca",
-            "s390x-unknown-linux-gnu": "eb86c655159d6f7b5fb245d9017f23aa388b5423f21caefeaee54469446ef9f2",
-            "x86_64-apple-darwin": "a618c086e0514f681523947e2b66a4dc0c6560f91c36faa072fa6787455df9ea",
-            "x86_64-pc-windows-msvc": "2cab4d2ee0c9313923c9b11297e23b1876ecb79ce6ad6de0b8b48baf8519ab67",
-            "x86_64-unknown-linux-gnu": "ff121f14ed113c9da83a45f76c3cf41976fb4419fe406d5cc7066765761c6a4e",
+            "aarch64-apple-darwin": "5a69382da99c4620690643517ca1f1f53772331b347e75f536088c42a4cf6620",
+            "aarch64-unknown-linux-gnu": "803e49259280af0f5466d32829cd9d65a302b0226e424b3f0b261f9daf6aee8f",
+            "ppc64le-unknown-linux-gnu": "92b666d103902001322f42badbd68da92adc5cebb826af9c1c906c33166e2f34",
+            "s390x-unknown-linux-gnu": "6d584317651c1ad4a857cb32d1999707e8bb3046fcb2f156d80381814fa19fde",
+            "x86_64-apple-darwin": "1e23ffe5bc473e1323ab8f51464da62d77399afb423babf67f8e13c82b69c674",
+            "x86_64-pc-windows-msvc": "647b66ff4552e70aec3bf634dd470891b4a2b291e8e8715b3bdb162f577d4c55",
+            "x86_64-unknown-linux-gnu": "8b50a442b04724a24c1eebb65a36a0c0e833d35374dbdf9c9470d8a97b164cd9",
         },
         "strip_prefix": "python",
     },
@@ -548,28 +549,35 @@
         "strip_prefix": "python",
     },
     "3.12.7": {
-        "url": "20241008/cpython-{python_version}+20241008-{platform}-{build}.tar.gz",
+        "url": "20241016/cpython-{python_version}+20241016-{platform}-{build}.tar.gz",
         "sha256": {
-            "aarch64-apple-darwin": "dd07d467f1d533b93d06e4d2ff88b91f491329510c6434297b88b584641bff5d",
-            "aarch64-unknown-linux-gnu": "ce3230da53aacb17ff77e912170786f47db4a446d4acb6cde7c397953a032bca",
-            "ppc64le-unknown-linux-gnu": "27d3cba42e94593c49f8610dcadd74f5b731c78f04ebabc2b0e1ba031ec09441",
-            "s390x-unknown-linux-gnu": "1e28e0fc9cd1fa0365a149c715c44d3030b2c989ca397fc074809b943449df41",
-            "x86_64-apple-darwin": "2347bf53ed3623645bed35adfca950b2c5291e3a759ec6c7765aa707b5dc866b",
-            "x86_64-pc-windows-msvc": "4ed1a146c66c7dbd85b87df69b17afc166ea7d70056aaf59a49c3d987a030d3b",
-            "x86_64-unknown-linux-gnu": "adbda1f3b77d7b65a551206e34a225375f408f9823e2e11df4c332aaecb8714b",
+            "aarch64-apple-darwin": "4c18852bf9c1a11b56f21bcf0df1946f7e98ee43e9e4c0c5374b2b3765cf9508",
+            "aarch64-unknown-linux-gnu": "bba3c6be6153f715f2941da34f3a6a69c2d0035c9c5396bc5bb68c6d2bd1065a",
+            "ppc64le-unknown-linux-gnu": "0a1d1d92e33a969bd2f40a80af53c97b6c0cc1060d384ceff50ff801593bf9d6",
+            "s390x-unknown-linux-gnu": "935676a0c960b552f95e9ac2e1e385de5de4b34038ff65ffdc688838f1189c17",
+            "x86_64-apple-darwin": "60c5271e7edc3c2ab47440b7abf4ed50fbc693880b474f74f05768f5b657045a",
+            "x86_64-pc-windows-msvc": "f05531bff16fa77b53be0776587b97b466070e768e6d5920894de988bdcd547a",
+            "x86_64-unknown-linux-gnu": "43576f7db1033dd57b900307f09c2e86f371152ac8a2607133afa51cbfc36064",
         },
         "strip_prefix": "python",
     },
     "3.13.0": {
-        "url": "20241008/cpython-{python_version}+20241008-{platform}-{build}.tar.gz",
+        "url": "20241016/cpython-{python_version}+20241016-{platform}-{build}.{ext}",
         "sha256": {
-            "aarch64-apple-darwin": "5d3cb8d7ca4cfbbe7ae1f118f26be112ee417d982fab8c6d85cfd8ccccf70718",
-            "aarch64-unknown-linux-gnu": "c1142af8f2c85923d2ba8201a35b913bb903a5d15f052c38bbecf2f49e2342dc",
-            "ppc64le-unknown-linux-gnu": "1be64a330499fed4e1f864b97eef5445b0e4abc0559ae45df3108981800cf998",
-            "s390x-unknown-linux-gnu": "c0b1cc51426feadaa932fdd9afd9a9af789916e128e48ac8909f9a269bbbd749",
-            "x86_64-apple-darwin": "b58ca12d9ae14bbd79f9e5cf4b748211ff1953e59abeac63b0f4e8e49845669f",
-            "x86_64-pc-windows-msvc": "c7651a7a575104f47c808902b020168057f3ad80f277e54cecfaf79a9ff50e22",
-            "x86_64-unknown-linux-gnu": "455200e1a202e9d9ef4b630c04af701c0a91dcaa6462022efc76893fc762ec95",
+            "aarch64-apple-darwin": "31397953849d275aa2506580f3fa1cb5a85b6a3d392e495f8030e8b6412f5556",
+            "aarch64-unknown-linux-gnu": "e8378c0162b2e0e4cc1f62b29443a3305d116d09583304dbb0149fecaff6347b",
+            "ppc64le-unknown-linux-gnu": "fc4b7f27c4e84c78f3c8e6c7f8e4023e4638d11f1b36b6b5ce457b1926cebb53",
+            "s390x-unknown-linux-gnu": "66b19e6a07717f6cfcd3a8ca953f0a2eaa232291142f3d26a8d17c979ec0f467",
+            "x86_64-apple-darwin": "cff1b7e7cd26f2d47acac1ad6590e27d29829776f77e8afa067e9419f2f6ce77",
+            "x86_64-pc-windows-msvc": "b25926e8ce4164cf103bacc4f4d154894ea53e07dd3fdd5ebb16fb1a82a7b1a0",
+            "x86_64-unknown-linux-gnu": "2c8cb15c6a2caadaa98af51df6fe78a8155b8471cb3dd7b9836038e0d3657fb4",
+            "aarch64-apple-darwin-freethreaded": "efc2e71c0e05bc5bedb7a846e05f28dd26491b1744ded35ed82f8b49ccfa684b",
+            "aarch64-unknown-linux-gnu-freethreaded": "59b50df9826475d24bb7eff781fa3949112b5e9c92adb29e96a09cdf1216d5bd",
+            "ppc64le-unknown-linux-gnu-freethreaded": "1217efa5f4ce67fcc9f7eb64165b1bd0912b2a21bc25c1a7e2cb174a21a5df7e",
+            "s390x-unknown-linux-gnu-freethreaded": "6c3e1e4f19d2b018b65a7e3ef4cd4225c5b9adfbc490218628466e636d5c4b8c",
+            "x86_64-apple-darwin-freethreaded": "2e07dfea62fe2215738551a179c87dbed1cc79d1b3654f4d7559889a6d5ce4eb",
+            "x86_64-pc-windows-msvc-freethreaded": "bfd89f9acf866463bc4baf01733da5e767d13f5d0112175a4f57ba91f1541310",
+            "x86_64-unknown-linux-gnu-freethreaded": "a73adeda301ad843cce05f31a2d3e76222b656984535a7b87696a24a098b216c",
         },
         "strip_prefix": "python",
     },
@@ -585,123 +593,145 @@
     "3.13": "3.13.0",
 }
 
-PLATFORMS = {
-    "aarch64-apple-darwin": struct(
-        compatible_with = [
-            "@platforms//os:macos",
-            "@platforms//cpu:aarch64",
-        ],
-        flag_values = {},
-        os_name = MACOS_NAME,
-        # Matches the value returned from:
-        # repository_ctx.execute(["uname", "-m"]).stdout.strip()
-        arch = "arm64",
-    ),
-    "aarch64-unknown-linux-gnu": struct(
-        compatible_with = [
-            "@platforms//os:linux",
-            "@platforms//cpu:aarch64",
-        ],
-        flag_values = {
-            Label("//python/config_settings:py_linux_libc"): "glibc",
-        },
-        os_name = LINUX_NAME,
-        # Note: this string differs between OSX and Linux
-        # Matches the value returned from:
-        # repository_ctx.execute(["uname", "-m"]).stdout.strip()
-        arch = "aarch64",
-    ),
-    "armv7-unknown-linux-gnu": struct(
-        compatible_with = [
-            "@platforms//os:linux",
-            "@platforms//cpu:armv7",
-        ],
-        flag_values = {
-            Label("//python/config_settings:py_linux_libc"): "glibc",
-        },
-        os_name = LINUX_NAME,
-        arch = "armv7",
-    ),
-    "i386-unknown-linux-gnu": struct(
-        compatible_with = [
-            "@platforms//os:linux",
-            "@platforms//cpu:i386",
-        ],
-        flag_values = {
-            Label("//python/config_settings:py_linux_libc"): "glibc",
-        },
-        os_name = LINUX_NAME,
-        arch = "i386",
-    ),
-    "ppc64le-unknown-linux-gnu": struct(
-        compatible_with = [
-            "@platforms//os:linux",
-            "@platforms//cpu:ppc",
-        ],
-        flag_values = {
-            Label("//python/config_settings:py_linux_libc"): "glibc",
-        },
-        os_name = LINUX_NAME,
-        # Note: this string differs between OSX and Linux
-        # Matches the value returned from:
-        # repository_ctx.execute(["uname", "-m"]).stdout.strip()
-        arch = "ppc64le",
-    ),
-    "riscv64-unknown-linux-gnu": struct(
-        compatible_with = [
-            "@platforms//os:linux",
-            "@platforms//cpu:riscv64",
-        ],
-        flag_values = {
-            Label("//python/config_settings:py_linux_libc"): "glibc",
-        },
-        os_name = LINUX_NAME,
-        arch = "riscv64",
-    ),
-    "s390x-unknown-linux-gnu": struct(
-        compatible_with = [
-            "@platforms//os:linux",
-            "@platforms//cpu:s390x",
-        ],
-        flag_values = {
-            Label("//python/config_settings:py_linux_libc"): "glibc",
-        },
-        os_name = LINUX_NAME,
-        # Note: this string differs between OSX and Linux
-        # Matches the value returned from:
-        # repository_ctx.execute(["uname", "-m"]).stdout.strip()
-        arch = "s390x",
-    ),
-    "x86_64-apple-darwin": struct(
-        compatible_with = [
-            "@platforms//os:macos",
-            "@platforms//cpu:x86_64",
-        ],
-        flag_values = {},
-        os_name = MACOS_NAME,
-        arch = "x86_64",
-    ),
-    "x86_64-pc-windows-msvc": struct(
-        compatible_with = [
-            "@platforms//os:windows",
-            "@platforms//cpu:x86_64",
-        ],
-        flag_values = {},
-        os_name = WINDOWS_NAME,
-        arch = "x86_64",
-    ),
-    "x86_64-unknown-linux-gnu": struct(
-        compatible_with = [
-            "@platforms//os:linux",
-            "@platforms//cpu:x86_64",
-        ],
-        flag_values = {
-            Label("//python/config_settings:py_linux_libc"): "glibc",
-        },
-        os_name = LINUX_NAME,
-        arch = "x86_64",
-    ),
-}
+def _generate_platforms():
+    libc = Label("//python/config_settings:py_linux_libc")
+
+    platforms = {
+        "aarch64-apple-darwin": struct(
+            compatible_with = [
+                "@platforms//os:macos",
+                "@platforms//cpu:aarch64",
+            ],
+            flag_values = {},
+            os_name = MACOS_NAME,
+            # Matches the value returned from:
+            # repository_ctx.execute(["uname", "-m"]).stdout.strip()
+            arch = "arm64",
+        ),
+        "aarch64-unknown-linux-gnu": struct(
+            compatible_with = [
+                "@platforms//os:linux",
+                "@platforms//cpu:aarch64",
+            ],
+            flag_values = {
+                libc: "glibc",
+            },
+            os_name = LINUX_NAME,
+            # Note: this string differs between OSX and Linux
+            # Matches the value returned from:
+            # repository_ctx.execute(["uname", "-m"]).stdout.strip()
+            arch = "aarch64",
+        ),
+        "armv7-unknown-linux-gnu": struct(
+            compatible_with = [
+                "@platforms//os:linux",
+                "@platforms//cpu:armv7",
+            ],
+            flag_values = {
+                libc: "glibc",
+            },
+            os_name = LINUX_NAME,
+            arch = "armv7",
+        ),
+        "i386-unknown-linux-gnu": struct(
+            compatible_with = [
+                "@platforms//os:linux",
+                "@platforms//cpu:i386",
+            ],
+            flag_values = {
+                libc: "glibc",
+            },
+            os_name = LINUX_NAME,
+            arch = "i386",
+        ),
+        "ppc64le-unknown-linux-gnu": struct(
+            compatible_with = [
+                "@platforms//os:linux",
+                "@platforms//cpu:ppc",
+            ],
+            flag_values = {
+                libc: "glibc",
+            },
+            os_name = LINUX_NAME,
+            # Note: this string differs between OSX and Linux
+            # Matches the value returned from:
+            # repository_ctx.execute(["uname", "-m"]).stdout.strip()
+            arch = "ppc64le",
+        ),
+        "riscv64-unknown-linux-gnu": struct(
+            compatible_with = [
+                "@platforms//os:linux",
+                "@platforms//cpu:riscv64",
+            ],
+            flag_values = {
+                Label("//python/config_settings:py_linux_libc"): "glibc",
+            },
+            os_name = LINUX_NAME,
+            arch = "riscv64",
+        ),
+        "s390x-unknown-linux-gnu": struct(
+            compatible_with = [
+                "@platforms//os:linux",
+                "@platforms//cpu:s390x",
+            ],
+            flag_values = {
+                Label("//python/config_settings:py_linux_libc"): "glibc",
+            },
+            os_name = LINUX_NAME,
+            # Note: this string differs between OSX and Linux
+            # Matches the value returned from:
+            # repository_ctx.execute(["uname", "-m"]).stdout.strip()
+            arch = "s390x",
+        ),
+        "x86_64-apple-darwin": struct(
+            compatible_with = [
+                "@platforms//os:macos",
+                "@platforms//cpu:x86_64",
+            ],
+            flag_values = {},
+            os_name = MACOS_NAME,
+            arch = "x86_64",
+        ),
+        "x86_64-pc-windows-msvc": struct(
+            compatible_with = [
+                "@platforms//os:windows",
+                "@platforms//cpu:x86_64",
+            ],
+            flag_values = {},
+            os_name = WINDOWS_NAME,
+            arch = "x86_64",
+        ),
+        "x86_64-unknown-linux-gnu": struct(
+            compatible_with = [
+                "@platforms//os:linux",
+                "@platforms//cpu:x86_64",
+            ],
+            flag_values = {
+                libc: "glibc",
+            },
+            os_name = LINUX_NAME,
+            arch = "x86_64",
+        ),
+    }
+
+    freethreaded = Label("//python/config_settings:py_freethreaded")
+    return {
+        p + suffix: struct(
+            compatible_with = v.compatible_with,
+            flag_values = {
+                freethreaded: freethreaded_value,
+            } | v.flag_values,
+            os_name = v.os_name,
+            arch = v.arch,
+        )
+        for p, v in platforms.items()
+        for suffix, freethreaded_value in {
+            "": "no",
+            "-" + FREETHREADED: "yes",
+        }.items()
+    }
+
+PLATFORMS = _generate_platforms()
 
 def get_release_info(platform, python_version, base_url = DEFAULT_RELEASE_BASE_URL, tool_versions = TOOL_VERSIONS):
     """Resolve the release URL for the requested interpreter version
@@ -731,10 +761,32 @@
     release_filename = None
     rendered_urls = []
     for u in url:
+        p, _, _ = platform.partition("-" + FREETHREADED)
+
+        if FREETHREADED in platform:
+            build = "{}+{}-full".format(
+                FREETHREADED,
+                {
+                    "aarch64-apple-darwin": "pgo+lto",
+                    "aarch64-unknown-linux-gnu": "lto",
+                    "ppc64le-unknown-linux-gnu": "lto",
+                    "s390x-unknown-linux-gnu": "lto",
+                    "x86_64-apple-darwin": "pgo+lto",
+                    "x86_64-pc-windows-msvc": "pgo",
+                    "x86_64-unknown-linux-gnu": "pgo+lto",
+                }[p],
+            )
+        else:
+            build = "install_only"
+
+        if WINDOWS_NAME in platform:
+            build = "shared-" + build
+
         release_filename = u.format(
-            platform = platform,
+            platform = p,
             python_version = python_version,
-            build = "shared-install_only" if (WINDOWS_NAME in platform) else "install_only",
+            build = build,
+            ext = "tar.zst" if build.endswith("full") else "tar.gz",
         )
         if "://" in release_filename:  # is absolute url?
             rendered_urls.append(release_filename)
@@ -760,11 +812,18 @@
     return (release_filename, rendered_urls, strip_prefix, patches, patch_strip)
 
 def print_toolchains_checksums(name):
-    native.genrule(
-        name = name,
-        srcs = [],
-        outs = ["print_toolchains_checksums.sh"],
-        cmd = """\
+    """A macro to print checksums for a particular Python interpreter version.
+
+    Args:
+        name: {type}`str`: the name of the runnable target.
+    """
+    all_commands = []
+    by_version = {}
+    for python_version in TOOL_VERSIONS.keys():
+        by_version[python_version] = _commands_for_version(python_version)
+        all_commands.append(_commands_for_version(python_version))
+
+    template = """\
 cat > "$@" <<'EOF'
 #!/bin/bash
 
@@ -774,12 +833,20 @@
 
 {commands}
 EOF
-        """.format(
-            commands = "\n".join([
-                _commands_for_version(python_version)
-                for python_version in TOOL_VERSIONS.keys()
-            ]),
-        ),
+    """
+
+    native.genrule(
+        name = name,
+        srcs = [],
+        outs = ["print_toolchains_checksums.sh"],
+        cmd = select({
+            "//python/config_settings:is_python_{}".format(version): template.format(
+                commands = commands,
+            )
+            for version, commands in by_version.items()
+        } | {
+            "//conditions:default": template.format(commands = "\n".join(all_commands)),
+        }),
         executable = True,
     )
 
diff --git a/tools/private/update_deps/update_coverage_deps.py b/tools/private/update_deps/update_coverage_deps.py
index a856b7a..bbff67e 100755
--- a/tools/private/update_deps/update_coverage_deps.py
+++ b/tools/private/update_deps/update_coverage_deps.py
@@ -42,6 +42,10 @@
     "manylinux2014_aarch64": "aarch64-unknown-linux-gnu",
     "macosx_11_0_arm64": "aarch64-apple-darwin",
     "macosx_10_9_x86_64": "x86_64-apple-darwin",
+    ("t", "manylinux2014_x86_64"): "x86_64-unknown-linux-gnu-freethreaded",
+    ("t", "manylinux2014_aarch64"): "aarch64-unknown-linux-gnu-freethreaded",
+    ("t", "macosx_11_0_arm64"): "aarch64-apple-darwin-freethreaded",
+    ("t", "macosx_10_9_x86_64"): "x86_64-apple-darwin-freethreaded",
 }
 
 
@@ -87,10 +91,18 @@
         return "{{\n{}\n}}".format(textwrap.indent("\n".join(parts), prefix="    "))
 
 
-def _get_platforms(filename: str, name: str, version: str, python_version: str):
-    return filename[
-        len(f"{name}-{version}-{python_version}-{python_version}-") : -len(".whl")
-    ].split(".")
+def _get_platforms(filename: str, python_version: str):
+    name, _, tail = filename.partition("-")
+    version, _, tail = tail.partition("-")
+    got_python_version, _, tail = tail.partition("-")
+    if python_version != got_python_version:
+        return []
+    abi, _, tail = tail.partition("-")
+
+    platforms, _, tail = tail.rpartition(".")
+    platforms = platforms.split(".")
+
+    return [("t", p) for p in platforms] if abi.endswith("t") else platforms
 
 
 def _map(
@@ -172,8 +184,6 @@
 
         platforms = _get_platforms(
             u["filename"],
-            args.name,
-            args.version,
             u["python_version"],
         )