feat(bzlmod): Register a default toolchain (#1259)

This makes rules_python always provide a default toolchain when using
bzlmod.

Note that, unlike workspace builds, the default is not the local
system Python (`@bazel_tools//tools/python:autodetecting_toolchain`).

Instead, the default is a hermetic runtime, but no guarantees are made
about the particular version used. In practice, it will be the latest
available Python version.

Work towards #1233
diff --git a/.github/workflows/create_archive_and_notes.sh b/.github/workflows/create_archive_and_notes.sh
index 0c0c4ac..02279bc 100755
--- a/.github/workflows/create_archive_and_notes.sh
+++ b/.github/workflows/create_archive_and_notes.sh
@@ -40,20 +40,6 @@
 )
 
 use_repo(pip, "pip")
-
-# (Optional) Register a specific python toolchain instead of using the host version
-python = use_extension("@rules_python//python:extensions.bzl", "python")
-
-python.toolchain(
-    name = "python3_9",
-    python_version = "3.9",
-)
-
-use_repo(python, "python3_9_toolchains")
-
-register_toolchains(
-    "@python3_9_toolchains//:all",
-)
 \`\`\`
 
 ## Using WORKSPACE
diff --git a/BZLMOD_SUPPORT.md b/BZLMOD_SUPPORT.md
index 8efd0df..dbe5238 100644
--- a/BZLMOD_SUPPORT.md
+++ b/BZLMOD_SUPPORT.md
@@ -35,3 +35,27 @@
 2. Gazelle does not support finding deps in sub-modules.  For instance we can have a dep like ` "@our_other_module//other_module/pkg:lib",` in a `py_test` definition.
 
 Check ["issues"](/bazelbuild/rules_python/issues) for an up to date list.
+
+## Differences in behavior from WORKSPACE
+
+### Default toolchain is not the local system Python
+
+Under bzlmod, the default toolchain is no longer based on the locally installed
+system Python. Instead, a recent Python version using the pre-built,
+standalone runtimes are used.
+
+If you need the local system Python to be your toolchain, then it's suggested
+that you setup and configure your own toolchain and register it. Note that using
+the local system's Python is not advised because will vary between users and
+platforms.
+
+If you want to use the same toolchain as what WORKSPACE used, then manually
+register the builtin Bazel Python toolchain by doing
+`register_toolchains("@bazel_tools//tools/python:autodetecting_toolchain")`.
+**IMPORTANT: this should only be done in a root module, and may intefere with
+the toolchains rules_python registers**.
+
+NOTE: Regardless of your toolchain, due to
+[#691](https://github.com/bazelbuild/rules_python/issues/691), `rules_python`
+still relies on a local Python being available to bootstrap the program before
+handing over execution to the toolchain Python.
diff --git a/MODULE.bazel b/MODULE.bazel
index 5381ba1..6729d09 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -51,6 +51,15 @@
 # We need to do another use_extension call to expose the "pythons_hub"
 # repo.
 python = use_extension("@rules_python//python/extensions:python.bzl", "python")
+
+# The default toolchain to use if nobody configures a toolchain.
+# NOTE: This is not a stable version. It is provided for convenience, but will
+# change frequently to track the most recent Python version.
+# NOTE: The root module can override this.
+python.toolchain(
+    is_default = True,
+    python_version = "3.11",
+)
 use_repo(python, "pythons_hub")
 
 # This call registers the Python toolchains.
diff --git a/README.md b/README.md
index 6893a1d..cf4b04e 100644
--- a/README.md
+++ b/README.md
@@ -45,31 +45,52 @@
 
 ### Using bzlmod
 
+NOTE: bzlmod support is still experimental; APIs subject to change.
+
 To import rules_python in your project, you first need to add it to your
 `MODULE.bazel` file, using the snippet provided in the
 [release you choose](https://github.com/bazelbuild/rules_python/releases).
 
+Once the dependency is added, a Python toolchain will be automatically
+registered and you'll be able to create runnable programs and tests.
+
+
 #### Toolchain registration with bzlmod
 
-To register a hermetic Python toolchain rather than rely on a system-installed interpreter for runtime execution, you can add to the `MODULE.bazel` file:
+NOTE: bzlmod support is still experimental; APIs subject to change.
+
+A default toolchain is automatically configured for by depending on
+`rules_python`. Note, however, the version used tracks the most recent Python
+release and will change often.
+
+If you want to register specific Python versions, then use
+`python.toolchain()` for each version you need:
 
 ```starlark
-# Find the latest version number here: https://github.com/bazelbuild/rules_python/releases
-# and change the version number if needed in the line below.
-bazel_dep(name = "rules_python", version = "0.21.0")
+python = use_extension("@rules_python//python:extensions.bzl", "python")
 
+python.toolchain(
+    python_version = "3.9",
+)
+```
+
+### Using pip with bzlmod
+
+NOTE: bzlmod support is still experimental; APIs subject to change.
+
+To use dependencies from PyPI, the `pip.parse()` extension is used to
+convert a requirements file into Bazel dependencies.
+
+```starlark
 python = use_extension("@rules_python//python/extensions:python.bzl", "python")
 python.toolchain(
-    name = "python",
-    configure_coverage_tool = True,
-    is_default = True,
     python_version = "3.9",
 )
 
 interpreter = use_extension("@rules_python//python/extensions:interpreter.bzl", "interpreter")
 interpreter.install(
     name = "interpreter",
-    python_name = "python",
+    python_name = "python_3_9",
 )
 use_repo(interpreter, "interpreter")
 
diff --git a/python/extensions/python.bzl b/python/extensions/python.bzl
index bed6230..d7a466a 100644
--- a/python/extensions/python.bzl
+++ b/python/extensions/python.bzl
@@ -98,12 +98,20 @@
                 module_name = mod.name,
             )
 
-            # Only the root module is allowed to set the default toolchain
-            # to prevent submodules from clobbering each other.
-            # A single toolchain in the root module is treated as the default
-            # because it's unambigiuous.
+            # Only the root module and rules_python are allowed to specify the default
+            # toolchain for a couple reasons:
+            # * It prevents submodules from specifying different defaults and only
+            #   one of them winning.
+            # * rules_python needs to set a soft default in case the root module doesn't,
+            #   e.g. if the root module doesn't use Python itself.
+            # * The root module is allowed to override the rules_python default.
             if mod.is_root:
+                # A single toolchain is treated as the default because it's unambiguous.
                 is_default = toolchain_attr.is_default or len(mod.tags.toolchain) == 1
+            elif mod.name == "rules_python" and not default_toolchain:
+                # We don't do the len() check because we want the default that rules_python
+                # sets to be clearly visible.
+                is_default = toolchain_attr.is_default
             else:
                 is_default = False
 
@@ -129,8 +137,7 @@
     # A default toolchain is required so that the non-version-specific rules
     # are able to match a toolchain.
     if default_toolchain == None:
-        fail("No default toolchain found: exactly one toolchain must have " +
-             "is_default=True set")
+        fail("No default Python toolchain configured. Is rules_python missing `is_default=True`?")
 
     # The last toolchain in the BUILD file is set as the default
     # toolchain. We need the default last.