feat(toolchains): support runtime registration from manifest (#3802)

Currently, all supported Python runtime versions and their
platform-specific metadata (URLs, SHA256s, strip_prefix) must be
hardcoded in `python/versions.bzl`. This makes it slow and difficult
to adopt new Python versions or custom builds without updating
`rules_python` itself.

This PR introduces the ability to dynamically fetch and register
Python runtimes from a remote python-build-standalone (PBS) manifest
file (e.g., `SHA256SUMS`).

This is supported via two new attributes in `python.override`:
- `add_runtime_manifest_urls`: A list of URLs pointing to manifest
  files  to parse and register.
- `runtime_manifest_sha`: The SHA256 hash of the manifest file.

The manifest file format is the python-build-standalone SHA256SUMS
format (`SHA FILENAME`), extended to allow arbitrary URLs for the
filename (to allow more arbitrary locations).
diff --git a/.bazelrc.deleted_packages b/.bazelrc.deleted_packages
index 7256937..407fd1c 100644
--- a/.bazelrc.deleted_packages
+++ b/.bazelrc.deleted_packages
@@ -38,6 +38,7 @@
 common --deleted_packages=tests/integration/pip_parse/empty
 common --deleted_packages=tests/integration/pip_parse_isolated
 common --deleted_packages=tests/integration/py_cc_toolchain_registered
+common --deleted_packages=tests/integration/runtime_manifests
 common --deleted_packages=tests/integration/toolchain_target_settings
 common --deleted_packages=tests/integration/uv_lock
 common --deleted_packages=tests/modules/another_module
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 99876bd..43e05b6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -114,6 +114,9 @@
 
 {#v0-0-0-added}
 ### Added
+* (toolchains) Support dynamically fetching and registering Python runtimes
+  from a python-build-standalone manifest file using
+  `python.override(add_runtime_manifest_urls = ..., runtime_manifest_sha = ...)`.
 * (toolchain) Added {obj}`python.override.toolchain_target_settings` to allow
   adding `config_setting` labels to all registered toolchains.
 * (windows) Full venv support for Windows is available. Set
diff --git a/docs/toolchains.md b/docs/toolchains.md
index 09aaed4..80884ba 100644
--- a/docs/toolchains.md
+++ b/docs/toolchains.md
@@ -242,6 +242,9 @@
   {attr}`python.single_version_platform_override.coverage_tool`.
 * Adding additional Python versions via {bzl:obj}`python.single_version_override` or
   {bzl:obj}`python.single_version_platform_override`.
+* Adding additional Python versions dynamically from a manifest file or URL
+  via {attr}`python.override.add_runtime_manifest_files` or
+  {attr}`python.override.add_runtime_manifest_urls`.
 
 ### Registering custom runtimes
 
@@ -310,6 +313,73 @@
 `target_settings` with `single_version_platform_override`.
 :::
 
+### Registering runtimes from a manifest
+
+If you want to register multiple custom runtimes or versions at once, you can
+use a python-build-standalone manifest file. This is useful if you want to
+adopt new versions that are not yet built into `rules_python` without having
+to manually define each one using `single_version_platform_override`.
+
+To do this, specify the `add_runtime_manifest_files` or
+`add_runtime_manifest_urls` (and `runtime_manifest_sha`) attributes in
+`python.override` in your `MODULE.bazel`.
+
+In the example below, we register all runtimes available in a specific local
+or remote PBS release manifest:
+
+```starlark
+# File: MODULE.bazel
+python = use_extension("@rules_python//python/extensions:python.bzl", "python")
+python.override(
+    add_runtime_manifest_files = [
+        "@//:SHA256SUMS",
+    ],
+    add_runtime_manifest_urls = [
+        "https://github.com/astral-sh/python-build-standalone/releases/download/20260414/SHA256SUMS",
+    ],
+    base_url = "https://example.com/downloads",
+    runtime_manifest_sha = "ce18fdfd47c66830a40ea9b9e314a14b1636bbfd684501bc5ca1fc6d55a7933f",
+)
+```
+
+#### Manifest file format
+
+The manifest must be a plain text file where each line contains the SHA256 hash
+and the location of a runtime archive, separated by whitespace:
+
+```
+<sha256> <location>
+```
+
+The `<location>` can be either:
+- A relative filename (e.g.,
+  `cpython-3.10.20+20260414-x86_64-unknown-linux-gnu-install_only.tar.zst`).
+  In this case, the download URL is constructed by appending the filename to
+  the `base_url` attribute (if using `add_runtime_manifest_files`) or to the
+  parent directory of each URL in `add_runtime_manifest_urls` (treating them
+  as mirrors).
+- An absolute URL (e.g.,
+  `https://example.com/downloads/cpython-3.10.20+20260414-x86_64-unknown-linux-gnu-install_only.tar.zst`).
+  In this case, the URL is used directly to download the archive.
+
+In both cases, the filename or the last path segment of the URL must follow
+the standard python-build-standalone naming convention. `rules_python` parses
+this name to extract runtime metadata (such as Python version, target
+architecture, operating system, and libc).
+
+Notes:
+- `rules_python` will read or download the manifest, parse it, and
+  automatically register toolchains for all valid Python runtimes found in it
+  that match supported platforms.
+- Only runtimes matching known platforms in `rules_python` will be registered.
+
+:::{versionadded} VERSION_NEXT_FEATURE
+Added support for registering runtimes from a manifest using
+`add_runtime_manifest_files`, `add_runtime_manifest_urls`, and
+`runtime_manifest_sha` in `python.override`.
+:::
+
+
 ### Using defined toolchains from WORKSPACE
 
 It is possible to use toolchains defined in `MODULE.bazel` in `WORKSPACE`. For example,
diff --git a/python/private/BUILD.bazel b/python/private/BUILD.bazel
index 6ab3d54..b54c198 100644
--- a/python/private/BUILD.bazel
+++ b/python/private/BUILD.bazel
@@ -253,6 +253,11 @@
 )
 
 bzl_library(
+    name = "pbs_manifest_bzl",
+    srcs = ["pbs_manifest.bzl"],
+)
+
+bzl_library(
     name = "precompile_bzl",
     srcs = ["precompile.bzl"],
     deps = [
@@ -274,6 +279,7 @@
     srcs = ["python.bzl"],
     deps = [
         ":full_version_bzl",
+        ":pbs_manifest_bzl",
         ":platform_info_bzl",
         ":python_register_toolchains_bzl",
         ":pythons_hub_bzl",
diff --git a/python/private/pbs_manifest.bzl b/python/private/pbs_manifest.bzl
new file mode 100644
index 0000000..e343a80
--- /dev/null
+++ b/python/private/pbs_manifest.bzl
@@ -0,0 +1,153 @@
+"""Helper functions to parse python-build-standalone manifests."""
+
+def parse_filename(filename):
+    """Parses a python-build-standalone filename (or URL) into its components.
+
+    See https://gregoryszorc.com/docs/python-build-standalone/main/running.html
+
+    Example: cpython-3.10.20+20260414-x86_64_v2-unknown-linux-musl-lto-full.tar.zst
+
+    Args:
+      filename: The filename or URL of the python-build-standalone release asset.
+
+    Returns:
+      A dictionary of parsed components if parsed successfully, else None.
+    """
+    basename = filename.rpartition("/")[-1]
+    if basename.endswith(".tar.zst"):
+        name = basename.removesuffix(".tar.zst")
+    elif basename.endswith(".tar.gz"):
+        name = basename.removesuffix(".tar.gz")
+    else:
+        return None
+
+    if not name.startswith("cpython-"):
+        return None
+    name = name.removeprefix("cpython-")
+
+    left, plus, tail = name.partition("+")
+    if plus:
+        python_version = left
+        build_version, sep, rest = tail.partition("-")
+        if not sep:
+            return None
+    else:
+        python_version, sep, rest = left.partition("-")
+        if not sep:
+            return None
+        build_version = ""
+
+    arch, sep, rest = rest.partition("-")
+    if not sep:
+        return None
+
+    microarch = ""
+    arch_base, sep_v, microarch_num = arch.partition("_v")
+    if sep_v:
+        arch = arch_base
+        microarch = "v" + microarch_num
+
+    vendor, sep, rest = rest.partition("-")
+    if not sep:
+        return None
+
+    os, sep, rest = rest.partition("-")
+    if not sep:
+        return None
+
+    libc = ""
+    next_part, _, remaining = rest.partition("-")
+    if os == "linux" and next_part in ["gnu", "musl"]:
+        libc = next_part
+        flavor = remaining
+    elif os == "windows" and next_part == "msvc":
+        libc = next_part
+        flavor = remaining
+    else:
+        libc = ""
+        flavor = rest
+
+    freethreaded = False
+    if flavor.startswith("freethreaded+"):
+        freethreaded = True
+        flavor = flavor.removeprefix("freethreaded+")
+    elif flavor.startswith("freethreaded-"):
+        freethreaded = True
+        flavor = flavor.removeprefix("freethreaded-")
+    elif flavor == "freethreaded":
+        freethreaded = True
+        flavor = ""
+
+    archive_flavor = ""
+    if flavor.endswith("-full"):
+        archive_flavor = "full"
+        flavor = flavor.removesuffix("-full")
+    elif flavor == "full":
+        archive_flavor = "full"
+        flavor = ""
+    elif flavor.endswith("-install_only_stripped"):
+        archive_flavor = "install_only_stripped"
+        flavor = flavor.removesuffix("-install_only_stripped")
+    elif flavor == "install_only_stripped":
+        archive_flavor = "install_only_stripped"
+        flavor = ""
+    elif flavor.endswith("-install_only"):
+        archive_flavor = "install_only"
+        flavor = flavor.removesuffix("-install_only")
+    elif flavor == "install_only":
+        archive_flavor = "install_only"
+        flavor = ""
+
+    return {
+        "arch": arch,
+        "archive_flavor": archive_flavor,
+        "build_version": build_version,
+        "flavor": flavor,
+        "freethreaded": freethreaded,
+        "libc": libc,
+        "location": filename,
+        "microarch": microarch,
+        "os": os,
+        "python_version": python_version,
+        "vendor": vendor,
+    }
+
+def parse_sha_manifest(content):
+    """Parses the SHA256SUMS file content into a list of structs.
+
+    Args:
+      content: The raw content of the manifest file.
+
+    Returns:
+      A list of structs capturing the parsed components of each valid entry.
+      Each struct contains the following fields:
+        - arch: CPU architecture (e.g., "x86_64").
+        - archive_flavor: Release asset archive type (e.g., "full", "install_only").
+        - build_version: Standalone release date (e.g., "20260414").
+        - location: Full package filename or URL (e.g., "cpython-3.11.15..." or "https://...").
+        - flavor: Build configuration flavor (e.g., "install_only").
+        - freethreaded: Whether the build is free-threaded (boolean).
+        - libc: C library type (e.g., "gnu", "musl", "msvc", or "").
+        - microarch: Microarchitecture level (e.g., "v2", "v3", or "").
+        - os: Operating system (e.g., "linux", "darwin", "windows").
+        - python_version: Python semver version (e.g., "3.11.15").
+        - sha256: SHA256 integrity hash of the release asset.
+        - vendor: Platform vendor (e.g., "unknown", "apple").
+    """
+    results = []
+    for line in content.split("\n"):
+        line = line.strip()
+        if not line:
+            continue
+        parts = [p for p in line.split(" ") if p]
+        if len(parts) != 2:
+            continue
+        sha256, filename = parts
+
+        parsed = parse_filename(filename)
+        if parsed:
+            results.append(struct(
+                sha256 = sha256,
+                **parsed
+            ))
+    return results
diff --git a/python/private/python.bzl b/python/private/python.bzl
index 6abc81e..73b2d19 100644
--- a/python/private/python.bzl
+++ b/python/private/python.bzl
@@ -18,6 +18,7 @@
 load("//python:versions.bzl", "DEFAULT_RELEASE_BASE_URL", "PLATFORMS", "TOOL_VERSIONS")
 load(":auth.bzl", "AUTH_ATTRS")
 load(":full_version.bzl", "full_version")
+load(":pbs_manifest.bzl", "parse_sha_manifest")
 load(":platform_info.bzl", "platform_info")
 load(":python_register_toolchains.bzl", "python_register_toolchains")
 load(":pythons_hub.bzl", "hub_repo")
@@ -76,7 +77,7 @@
     # Map of string Major.Minor or Major.Minor.Patch to the toolchain_info struct
     global_toolchain_versions = {}
 
-    config = _get_toolchain_config(modules = module_ctx.modules, _fail = _fail)
+    config = _get_toolchain_config(mctx = module_ctx, modules = module_ctx.modules, _fail = _fail)
 
     default_python_version = _compute_default_python_version(module_ctx)
 
@@ -741,10 +742,89 @@
 
             override.fn(tag = tag, _fail = _fail, default = default)
 
-def _get_toolchain_config(*, modules, _fail = fail):
+def _populate_from_pbs_manifest(
+        *,
+        mctx,
+        add_runtime_manifest_urls = [],
+        add_runtime_manifest_files = [],
+        runtime_manifest_sha = "",
+        base_url = "",
+        available_versions,
+        _fail):
+    manifest_contents = []
+
+    if add_runtime_manifest_urls:
+        manifest_path = mctx.path("runtime_manifest")
+        result = mctx.download(
+            url = add_runtime_manifest_urls,
+            output = manifest_path,
+            sha256 = runtime_manifest_sha,
+        )
+        if not result.success:
+            _fail("Failed to download manifest from {}: {}".format(add_runtime_manifest_urls, result))
+            return
+        manifest_contents.append(mctx.read(manifest_path))
+
+    for manifest_file in add_runtime_manifest_files:
+        manifest_contents.append(mctx.read(manifest_file, watch = "yes"))
+
+    if not manifest_contents:
+        return
+
+    base_download_urls = [url.rpartition("/")[0] for url in add_runtime_manifest_urls]
+    if not base_download_urls and base_url:
+        base_download_urls = [base_url]
+
+    entries = []
+    for content in manifest_contents:
+        entries.extend(parse_sha_manifest(content))
+
+    # We don't model archive_flavor via flags yet, so have to pick one.
+    # Preference is given to install_only because its smaller
+    entries = sorted(
+        entries,
+        key = lambda e: {"full": 3, "install_only": 1, "install_only_stripped": 2}.get(e.archive_flavor, 4),
+    )
+
+    for entry in entries:
+        location = entry.location
+        sha256 = entry.sha256
+        py_version = entry.python_version
+
+        # Fallback to matching against PLATFORMS keys as before to ensure compatibility
+        # with rules_python expected platform keys.
+        matched_platform = None
+        for platform in PLATFORMS.keys():
+            if platform in location:
+                matched_platform = platform
+                break
+
+        if not matched_platform:
+            continue
+
+        if entry.archive_flavor not in ["install_only", "install_only_stripped", "full"]:
+            continue
+
+        v_dict = available_versions.setdefault(py_version, {})
+        if matched_platform in v_dict.get("sha256", {}):
+            continue
+
+        if "://" in location:
+            urls = [location]
+        else:
+            urls = ["{}/{}".format(b_url, location) for b_url in base_download_urls]
+
+        strip_prefix = "python/install" if entry.archive_flavor == "full" else "python"
+
+        v_dict.setdefault("sha256", {})[matched_platform] = sha256
+        v_dict.setdefault("url", {})[matched_platform] = urls
+        v_dict.setdefault("strip_prefix", {})[matched_platform] = strip_prefix
+
+def _get_toolchain_config(*, mctx, modules, _fail = fail):
     """Computes the configs for toolchains.
 
     Args:
+        mctx: The module context.
         modules: The modules from module_ctx
         _fail: Function to call for failing; only used for testing.
 
@@ -786,6 +866,21 @@
         else:
             available_versions[py_version]["url"] = dict(url)
 
+    # Check for add_runtime_manifest_urls or add_runtime_manifest_files in override tags in root module
+    root_module = modules[0] if modules else None
+    if root_module and root_module.is_root:
+        for tag in root_module.tags.override:
+            if tag.add_runtime_manifest_urls or tag.add_runtime_manifest_files:
+                _populate_from_pbs_manifest(
+                    mctx = mctx,
+                    add_runtime_manifest_urls = tag.add_runtime_manifest_urls,
+                    add_runtime_manifest_files = tag.add_runtime_manifest_files,
+                    runtime_manifest_sha = tag.runtime_manifest_sha,
+                    base_url = tag.base_url,
+                    available_versions = available_versions,
+                    _fail = _fail,
+                )
+
     default = {
         "base_url": DEFAULT_RELEASE_BASE_URL,
         "platforms": dict(PLATFORMS),  # Copy so it's mutable.
@@ -1111,6 +1206,34 @@
 :::
 """,
     attrs = {
+        "add_runtime_manifest_files": attr.label_list(
+            mandatory = False,
+            allow_files = True,
+            doc = """
+Labels pointing to local python-build-standalone manifest files (e.g., `SHA256SUMS`).
+
+Example:
+`//my/custom/manifest:SHA256SUMS`
+
+:::{versionadded} VERSION_NEXT_FEATURE
+:::
+""",
+        ),
+        "add_runtime_manifest_urls": attr.string_list(
+            mandatory = False,
+            doc = """
+URLs pointing to python-build-standalone manifest files (e.g., SHA256SUMS).
+
+Example:
+`https://github.com/astral-sh/python-build-standalone/releases/download/20260414/SHA256SUMS`
+
+Note that `/latest/` can be used in place of a specific release date (e.g., `20260414`) to automatically use the latest release:
+`https://github.com/astral-sh/python-build-standalone/releases/latest/download/SHA256SUMS`
+
+:::{versionadded} VERSION_NEXT_FEATURE
+:::
+""",
+        ),
         "add_target_settings": attr.string_list(
             mandatory = False,
             doc = """\
@@ -1180,6 +1303,15 @@
             default = {},
         ),
         "register_all_versions": attr.bool(default = False, doc = "Add all versions"),
+        "runtime_manifest_sha": attr.string(
+            mandatory = False,
+            doc = """
+SHA256 hash for the add_runtime_manifest_urls.
+
+:::{versionadded} VERSION_NEXT_FEATURE
+:::
+""",
+        ),
     } | AUTH_ATTRS,
 )
 
diff --git a/tests/integration/BUILD.bazel b/tests/integration/BUILD.bazel
index a6027fc..904fb4c 100644
--- a/tests/integration/BUILD.bazel
+++ b/tests/integration/BUILD.bazel
@@ -105,6 +105,10 @@
 )
 
 rules_python_integration_test(
+    name = "runtime_manifests_test",
+)
+
+rules_python_integration_test(
     name = "custom_commands_test",
     py_main = "custom_commands_test.py",
 )
diff --git a/tests/integration/runtime_manifests/.bazelrc b/tests/integration/runtime_manifests/.bazelrc
new file mode 100644
index 0000000..d4d45a5
--- /dev/null
+++ b/tests/integration/runtime_manifests/.bazelrc
@@ -0,0 +1,4 @@
+# Copy of fast-tests config
+common:fast-tests --build_tests_only=true
+common:fast-tests --build_tag_filters=-large,-enormous,-integration-test
+common:fast-tests --test_tag_filters=-large,-enormous,-integration-test
diff --git a/tests/integration/runtime_manifests/BUILD.bazel b/tests/integration/runtime_manifests/BUILD.bazel
new file mode 100644
index 0000000..4746fd4
--- /dev/null
+++ b/tests/integration/runtime_manifests/BUILD.bazel
@@ -0,0 +1,7 @@
+load("@rules_python//python:py_test.bzl", "py_test")
+
+py_test(
+    name = "basic_test",
+    srcs = ["basic_test.py"],
+    python_version = "3.11",
+)
diff --git a/tests/integration/runtime_manifests/MODULE.bazel b/tests/integration/runtime_manifests/MODULE.bazel
new file mode 100644
index 0000000..6891b07
--- /dev/null
+++ b/tests/integration/runtime_manifests/MODULE.bazel
@@ -0,0 +1,19 @@
+module(name = "runtime_manifests")
+
+bazel_dep(name = "rules_python", version = "0.0.0")
+local_path_override(
+    module_name = "rules_python",
+    path = "../../..",
+)
+
+python = use_extension("@rules_python//python/extensions:python.bzl", "python")
+python.override(
+    add_runtime_manifest_urls = [
+        "https://github.com/astral-sh/python-build-standalone/releases/download/20260414/SHA256SUMS",
+    ],
+    register_all_versions = True,
+    runtime_manifest_sha = "ce18fdfd47c66830a40ea9b9e314a14b1636bbfd684501bc5ca1fc6d55a7933f",
+)
+python.toolchain(
+    python_version = "3.11.15",
+)
diff --git a/tests/integration/runtime_manifests/WORKSPACE b/tests/integration/runtime_manifests/WORKSPACE
new file mode 100644
index 0000000..8277ec8
--- /dev/null
+++ b/tests/integration/runtime_manifests/WORKSPACE
@@ -0,0 +1 @@
+# Workspace boundary file required by rules_bazel_integration_test
diff --git a/tests/integration/runtime_manifests/basic_test.py b/tests/integration/runtime_manifests/basic_test.py
new file mode 100644
index 0000000..35f0e93
--- /dev/null
+++ b/tests/integration/runtime_manifests/basic_test.py
@@ -0,0 +1,27 @@
+import datetime
+import platform
+import sys
+import unittest
+
+
+class BasicTest(unittest.TestCase):
+    def test_basic(self):
+        print("Hello World from Python {}!".format(sys.version))
+        print("Interpreter executable path: {}".format(sys.executable))
+
+        # Verify that the hermetic interpreter inside Bazel's output/sandbox tree is used
+        self.assertIn(".cache/bazel", sys.executable)
+
+        # Verify that the exact custom version (3.11.15) parsed from the manifest is used
+        self.assertEqual(sys.version_info[:3], (3, 11, 15))
+
+        # Verify that the exact build version (20260414) parsed from the manifest is used
+        buildno, builddate = platform.python_build()
+        date_str = " ".join(builddate.split()[:3])
+        dt = datetime.datetime.strptime(date_str, "%b %d %Y")
+        formatted_date = dt.strftime("%Y%m%d")
+        self.assertEqual(formatted_date, "20260414")
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/python/BUILD.bazel b/tests/python/BUILD.bazel
index 2553536..887fe96 100644
--- a/tests/python/BUILD.bazel
+++ b/tests/python/BUILD.bazel
@@ -12,6 +12,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-load(":python_tests.bzl", "python_test_suite")
+load(":python_tests.bzl", "register_python_tests")
 
-python_test_suite(name = "python_tests")
+register_python_tests(name = "python_tests")
diff --git a/tests/python/python_tests.bzl b/tests/python/python_tests.bzl
index 5db7426..cd73839 100644
--- a/tests/python/python_tests.bzl
+++ b/tests/python/python_tests.bzl
@@ -16,6 +16,7 @@
 
 load("@pythons_hub//:versions.bzl", "MINOR_MAPPING")
 load("@rules_testing//lib:test_suite.bzl", "test_suite")
+load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED")  # buildifier: disable=bzl-visibility
 load("//python/private:python.bzl", "parse_modules")  # buildifier: disable=bzl-visibility
 load("//python/private:repo_utils.bzl", "repo_utils")  # buildifier: disable=bzl-visibility
 load("//tests/support/mocks:mocks.bzl", "mocks")
@@ -878,3 +879,17 @@
         name: the name of the test suite
     """
     test_suite(name = name, basic_tests = _tests)
+
+def register_python_tests(name):
+    """Registers the python tests if Bzlmod is enabled, otherwise defines an empty test_suite.
+
+    Args:
+        name: The name of the test target.
+    """
+    if BZLMOD_ENABLED:
+        python_test_suite(name = name)
+    else:
+        native.test_suite(
+            name = name,
+            tests = [],
+        )
diff --git a/tests/python_bzlmod_ext/BUILD.bazel b/tests/python_bzlmod_ext/BUILD.bazel
new file mode 100644
index 0000000..0add4e7
--- /dev/null
+++ b/tests/python_bzlmod_ext/BUILD.bazel
@@ -0,0 +1,7 @@
+load(":test_helpers.bzl", "register_python_bzlmod_ext_tests")
+
+register_python_bzlmod_ext_tests(
+    name = "python_bzlmod_ext_tests",
+    parse_sha_manifest_name = "parse_sha_manifest_tests",
+    runtime_manifests_name = "runtime_manifests_tests",
+)
diff --git a/tests/python_bzlmod_ext/parse_sha_manifest_tests.bzl b/tests/python_bzlmod_ext/parse_sha_manifest_tests.bzl
new file mode 100644
index 0000000..4ddcdcc
--- /dev/null
+++ b/tests/python_bzlmod_ext/parse_sha_manifest_tests.bzl
@@ -0,0 +1,183 @@
+"""Tests for manifest parsing Starlark functions."""
+
+load("@bazel_skylib//lib:structs.bzl", "structs")
+load("@rules_testing//lib:analysis_test.bzl", "analysis_test")
+load("@rules_testing//lib:test_suite.bzl", "test_suite")
+load("@rules_testing//lib:util.bzl", rt_util = "util")
+load("//python/private:pbs_manifest.bzl", "parse_filename", "parse_sha_manifest")  # buildifier: disable=bzl-visibility
+
+_tests = []
+
+def _test_parse_filename_baseline(name):
+    """Sets up the baseline filename parsing test.
+
+    Args:
+      name: The name of the test.
+    """
+    rt_util.helper_target(
+        native.filegroup,
+        name = name + "_subject",
+    )
+    analysis_test(
+        name = name,
+        target = name + "_subject",
+        impl = _test_parse_filename_baseline_impl,
+    )
+
+def _test_parse_filename_baseline_impl(env, target):
+    _ = target  # @unused
+
+    # 1. Baseline
+    parsed1 = parse_filename("cpython-3.11.15+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz")
+    env.expect.that_dict(parsed1).contains_exactly({
+        "arch": "x86_64",
+        "archive_flavor": "install_only",
+        "build_version": "20260414",
+        "flavor": "",
+        "freethreaded": False,
+        "libc": "gnu",
+        "location": "cpython-3.11.15+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz",
+        "microarch": "",
+        "os": "linux",
+        "python_version": "3.11.15",
+        "vendor": "unknown",
+    })
+
+    # 2. Microarch
+    parsed2 = parse_filename("cpython-3.10.20+20260414-x86_64_v2-unknown-linux-musl-lto-full.tar.zst")
+    env.expect.that_dict(parsed2).contains_exactly({
+        "arch": "x86_64",
+        "archive_flavor": "full",
+        "build_version": "20260414",
+        "flavor": "lto",
+        "freethreaded": False,
+        "libc": "musl",
+        "location": "cpython-3.10.20+20260414-x86_64_v2-unknown-linux-musl-lto-full.tar.zst",
+        "microarch": "v2",
+        "os": "linux",
+        "python_version": "3.10.20",
+        "vendor": "unknown",
+    })
+
+    # 3. Freethreaded
+    parsed3 = parse_filename("cpython-3.13.13+20260414-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst")
+    env.expect.that_dict(parsed3).contains_exactly({
+        "arch": "aarch64",
+        "archive_flavor": "full",
+        "build_version": "20260414",
+        "flavor": "pgo+lto",
+        "freethreaded": True,
+        "libc": "",
+        "location": "cpython-3.13.13+20260414-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst",
+        "microarch": "",
+        "os": "darwin",
+        "python_version": "3.13.13",
+        "vendor": "apple",
+    })
+
+    # 4. Invalid
+    parsed4 = parse_filename("invalid-filename.tar.gz")
+    env.expect.that_bool(parsed4 == None).equals(True)
+
+    # 5. Full URL (should return the original URL as location)
+    parsed5 = parse_filename("https://github.com/astral-sh/python-build-standalone/releases/download/20260414/cpython-3.11.15+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz")
+    env.expect.that_dict(parsed5).contains_exactly({
+        "arch": "x86_64",
+        "archive_flavor": "install_only",
+        "build_version": "20260414",
+        "flavor": "",
+        "freethreaded": False,
+        "libc": "gnu",
+        "location": "https://github.com/astral-sh/python-build-standalone/releases/download/20260414/cpython-3.11.15+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz",
+        "microarch": "",
+        "os": "linux",
+        "python_version": "3.11.15",
+        "vendor": "unknown",
+    })
+
+_tests.append(_test_parse_filename_baseline)
+
+def _test_parse_sha_manifest(name):
+    """Sets up the manifest file parsing test.
+
+    Args:
+      name: The name of the test.
+    """
+    rt_util.helper_target(
+        native.filegroup,
+        name = name + "_subject",
+    )
+    analysis_test(
+        name = name,
+        target = name + "_subject",
+        impl = _test_parse_sha_manifest_impl,
+    )
+
+def _test_parse_sha_manifest_impl(env, target):
+    _ = target  # @unused
+    content = """
+8b14030dd3af9ea7f7c51b4c90feb04afd8a8f45435727e67b875270bd08f3bc   cpython-3.11.15+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz
+a57ffd435652092d16b30e783f9826c55e9c64b0f0a72cbae0a9f39e663137fb       cpython-3.11.15+20260414-aarch64-apple-darwin-install_only.tar.gz
+ce18fdfd47c66830a40ea9b9e314a14b1636bbfd684501bc5ca1fc6d55a7933f  https://example.com/cpython-3.10.20+20260414-x86_64_v2-unknown-linux-musl-lto-full.tar.zst
+1111111111111111111111111111111111111111111111111111111111111111  cpython-3.13.13+20260414-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst
+"""
+    parsed = parse_sha_manifest(content)
+    env.expect.that_collection(parsed).has_size(4)
+
+    env.expect.that_dict(structs.to_dict(parsed[0])).contains_exactly({
+        "arch": "x86_64",
+        "archive_flavor": "install_only",
+        "build_version": "20260414",
+        "flavor": "",
+        "freethreaded": False,
+        "libc": "gnu",
+        "location": "cpython-3.11.15+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz",
+        "microarch": "",
+        "os": "linux",
+        "python_version": "3.11.15",
+        "sha256": "8b14030dd3af9ea7f7c51b4c90feb04afd8a8f45435727e67b875270bd08f3bc",
+        "vendor": "unknown",
+    })
+
+    env.expect.that_dict(structs.to_dict(parsed[2])).contains_exactly({
+        "arch": "x86_64",
+        "archive_flavor": "full",
+        "build_version": "20260414",
+        "flavor": "lto",
+        "freethreaded": False,
+        "libc": "musl",
+        "location": "https://example.com/cpython-3.10.20+20260414-x86_64_v2-unknown-linux-musl-lto-full.tar.zst",
+        "microarch": "v2",
+        "os": "linux",
+        "python_version": "3.10.20",
+        "sha256": "ce18fdfd47c66830a40ea9b9e314a14b1636bbfd684501bc5ca1fc6d55a7933f",
+        "vendor": "unknown",
+    })
+
+    env.expect.that_dict(structs.to_dict(parsed[3])).contains_exactly({
+        "arch": "aarch64",
+        "archive_flavor": "full",
+        "build_version": "20260414",
+        "flavor": "pgo+lto",
+        "freethreaded": True,
+        "libc": "",
+        "location": "cpython-3.13.13+20260414-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst",
+        "microarch": "",
+        "os": "darwin",
+        "python_version": "3.13.13",
+        "sha256": "1111111111111111111111111111111111111111111111111111111111111111",
+        "vendor": "apple",
+    })
+
+_tests.append(_test_parse_sha_manifest)
+
+def parse_sha_manifest_test_suite(name):
+    """Defines the test suite for manifest parsing.
+
+    Args:
+      name: The name of the test suite.
+    """
+    test_suite(
+        name = name,
+        tests = _tests,
+    )
diff --git a/tests/python_bzlmod_ext/runtime_manifests_tests.bzl b/tests/python_bzlmod_ext/runtime_manifests_tests.bzl
new file mode 100644
index 0000000..c46cab1
--- /dev/null
+++ b/tests/python_bzlmod_ext/runtime_manifests_tests.bzl
@@ -0,0 +1,163 @@
+"""Starlark unit tests for dynamic toolchain registration via manifests."""
+
+load("@rules_testing//lib:analysis_test.bzl", "analysis_test")
+load("@rules_testing//lib:test_suite.bzl", "test_suite")
+load("@rules_testing//lib:util.bzl", rt_util = "util")
+load("//python/private:python.bzl", "parse_modules")  # buildifier: disable=bzl-visibility
+load("//python/private:repo_utils.bzl", "repo_utils")  # buildifier: disable=bzl-visibility
+load("//tests/support/mocks:mocks.bzl", "mocks")  # buildifier: disable=bzl-visibility
+load("//tests/support/mocks:python_ext.bzl", "python_ext")  # buildifier: disable=bzl-visibility
+
+_tests = []
+
+_mock_logger = repo_utils.logger(
+    name = "mock",
+    verbosity_level = "ERROR",
+)
+
+def _test_dynamic_manifest_toolchains(name):
+    rt_util.helper_target(
+        native.filegroup,
+        name = name + "_subject",
+    )
+    analysis_test(
+        name = name,
+        target = name + "_subject",
+        impl = _test_dynamic_manifest_toolchains_impl,
+    )
+
+def _test_dynamic_manifest_toolchains_impl(env, target):
+    _ = target  # @unused
+
+    # Construct Bzlmod mock module locally inside the test execution block.
+    # We test using virtual patch version "3.11.99" (not present in TOOL_VERSIONS)
+    # so that the populated config contains ONLY our dynamically parsed manifest keys
+    # without any pre-populated multi-platform templates, allowing exact dictionary match!
+    root_module = python_ext.module(
+        name = "runtime_manifests",
+        override = [
+            python_ext.override(
+                add_runtime_manifest_urls = [
+                    "https://github.com/astral-sh/python-build-standalone/releases/download/20260414/SHA256SUMS",
+                ],
+                runtime_manifest_sha = "ce18fdfd47c66830a40ea9b9e314a14b1636bbfd684501bc5ca1fc6d55a7933f",
+                register_all_versions = True,
+            ),
+        ],
+        defaults = [
+            python_ext.defaults(
+                python_version = "3.11.99",
+            ),
+        ],
+    )
+
+    # Pre-populate mock_files directly to bypass download output struct key mismatch in mock read lookups.
+    mock_mctx = mocks.mctx(
+        modules = [root_module],
+        mock_files = {
+            "runtime_manifest": """
+01e607cf764b97d4d5d6f69fd1ff3d8a9a162513dde5c39e98260fce40fe220a  cpython-3.11.99+20260414-x86_64-unknown-linux-gnu-pgo+lto-full.tar.zst
+8b14030dd3af9ea7f7c51b4c90feb04afd8a8f45435727e67b875270bd08f3bc  cpython-3.11.99+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz
+""",
+        },
+    )
+
+    res = parse_modules(
+        module_ctx = mock_mctx,
+        logger = _mock_logger,
+    )
+
+    tool_versions = res.config.default["tool_versions"]
+    env.expect.that_bool("3.11.99" in tool_versions).equals(True)
+
+    version_info = tool_versions["3.11.99"]
+
+    # Assert on the entire dictionary at once!
+    env.expect.that_dict(version_info).contains_exactly({
+        "sha256": {
+            "x86_64-unknown-linux-gnu": "8b14030dd3af9ea7f7c51b4c90feb04afd8a8f45435727e67b875270bd08f3bc",
+        },
+        "strip_prefix": {
+            "x86_64-unknown-linux-gnu": "python",
+        },
+        "url": {
+            "x86_64-unknown-linux-gnu": [
+                "https://github.com/astral-sh/python-build-standalone/releases/download/20260414/cpython-3.11.99+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz",
+            ],
+        },
+    })
+
+_tests.append(_test_dynamic_manifest_toolchains)
+
+def _test_dynamic_manifest_files(name):
+    rt_util.helper_target(
+        native.filegroup,
+        name = name + "_subject",
+    )
+    analysis_test(
+        name = name,
+        target = name + "_subject",
+        impl = _test_dynamic_manifest_files_impl,
+    )
+
+def _test_dynamic_manifest_files_impl(env, target):
+    _ = target  # @unused
+
+    root_module = python_ext.module(
+        name = "runtime_manifests",
+        override = [
+            python_ext.override(
+                add_runtime_manifest_files = [
+                    Label("//:SHA256SUMS"),
+                ],
+                base_url = "https://example.com/dl",
+                register_all_versions = True,
+            ),
+        ],
+        defaults = [
+            python_ext.defaults(
+                python_version = "3.12.99",
+            ),
+        ],
+    )
+
+    mock_mctx = mocks.mctx(
+        modules = [root_module],
+        mock_files = {
+            str(Label("//:SHA256SUMS")): """
+01e607cf764b97d4d5d6f69fd1ff3d8a9a162513dde5c39e98260fce40fe220a  cpython-3.12.99+20260414-x86_64-unknown-linux-gnu-pgo+lto-full.tar.zst
+""",
+        },
+    )
+
+    res = parse_modules(
+        module_ctx = mock_mctx,
+        logger = _mock_logger,
+    )
+
+    tool_versions = res.config.default["tool_versions"]
+    env.expect.that_bool("3.12.99" in tool_versions).equals(True)
+
+    version_info = tool_versions["3.12.99"]
+
+    env.expect.that_dict(version_info).contains_exactly({
+        "sha256": {
+            "x86_64-unknown-linux-gnu": "01e607cf764b97d4d5d6f69fd1ff3d8a9a162513dde5c39e98260fce40fe220a",
+        },
+        "strip_prefix": {
+            "x86_64-unknown-linux-gnu": "python/install",
+        },
+        "url": {
+            "x86_64-unknown-linux-gnu": [
+                "https://example.com/dl/cpython-3.12.99+20260414-x86_64-unknown-linux-gnu-pgo+lto-full.tar.zst",
+            ],
+        },
+    })
+
+_tests.append(_test_dynamic_manifest_files)
+
+def runtime_manifests_test_suite(name):
+    test_suite(
+        name = name,
+        tests = _tests,
+    )
diff --git a/tests/python_bzlmod_ext/test_helpers.bzl b/tests/python_bzlmod_ext/test_helpers.bzl
new file mode 100644
index 0000000..f6c1776
--- /dev/null
+++ b/tests/python_bzlmod_ext/test_helpers.bzl
@@ -0,0 +1,48 @@
+# Copyright 2026 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Helpers to conditionally register tests depending on Bzlmod enablement."""
+
+load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED")  # buildifier: disable=bzl-visibility
+load(":parse_sha_manifest_tests.bzl", "parse_sha_manifest_test_suite")
+load(":runtime_manifests_tests.bzl", "runtime_manifests_test_suite")
+
+def register_python_bzlmod_ext_tests(name, parse_sha_manifest_name, runtime_manifests_name):
+    """Registers the Bzlmod extension tests if Bzlmod is enabled, otherwise defines empty test_suites.
+
+    Args:
+        name: The name of the master test_suite target.
+        parse_sha_manifest_name: The name of the parse_sha_manifest test target.
+        runtime_manifests_name: The name of the runtime_manifests test target.
+    """
+    if BZLMOD_ENABLED:
+        parse_sha_manifest_test_suite(name = parse_sha_manifest_name)
+        runtime_manifests_test_suite(name = runtime_manifests_name)
+    else:
+        native.test_suite(
+            name = parse_sha_manifest_name,
+            tests = [],
+        )
+        native.test_suite(
+            name = runtime_manifests_name,
+            tests = [],
+        )
+
+    native.test_suite(
+        name = name,
+        tests = [
+            parse_sha_manifest_name,
+            runtime_manifests_name,
+        ],
+    )
diff --git a/tests/support/mocks/python_ext.bzl b/tests/support/mocks/python_ext.bzl
index f20a6c7..f7b5b0e 100644
--- a/tests/support/mocks/python_ext.bzl
+++ b/tests/support/mocks/python_ext.bzl
@@ -26,6 +26,7 @@
 def _override(**kwargs):
     """Creates a mock python.override tag with default values."""
     attrs = {
+        "add_runtime_manifest_files": [],
         "add_runtime_manifest_urls": [],
         "add_target_settings": [],
         "available_python_versions": [],
diff --git a/tools/private/debug/print_defined_toolchains.sh b/tools/private/debug/print_defined_toolchains.sh
new file mode 100755
index 0000000..1694dc9
--- /dev/null
+++ b/tools/private/debug/print_defined_toolchains.sh
@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# Programmatically probe which repository target name is resolved successfully inside this workspace
+if bazel query @pythons_hub//... >/dev/null 2>&1; then
+  HUB_REPO="@pythons_hub"
+elif bazel query @@rules_python++python+pythons_hub//... >/dev/null 2>&1; then
+  HUB_REPO="@@rules_python++python+pythons_hub"
+else
+  HUB_REPO="@@+python+pythons_hub"
+fi
+
+# Query standard toolchains inside the resolved hub repository, excluding CC and Exec Tools toolchains.
+bazel query "kind('toolchain', ${HUB_REPO}//...) - filter('_py_cc_toolchain$', ${HUB_REPO}//...) - filter('_py_exec_tools_toolchain$', ${HUB_REPO}//...)" "$@"