Add a python_version attribute to py_runtime and its associated provider

This is used to declare whether a runtime is for Python 2 or Python 3. When we move to toolchains, the runtime's declared version will be validated against the py_binary's version. For compatibility, validation will not occur for the old --python_top way of specifying runtimes.

Initially the attribute is optional on the py_runtime rule and mandatory on the PyRuntimeInfo provider. At some point we'll add an incompatible change to make the attribute mandatory on the rule too. In the meantime it defaults to a version in the same way as py_binary, i.e. with --incompatible_py3_is_default.

Work toward #7375.

RELNOTES: `py_runtime` gains a `python_version` attribute for specifying whether it represents a Python 2 or 3 interpreter.
PiperOrigin-RevId: 235774497
diff --git a/src/main/java/com/google/devtools/build/lib/rules/python/PyRuntime.java b/src/main/java/com/google/devtools/build/lib/rules/python/PyRuntime.java
index c5a904e..4459f39 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/python/PyRuntime.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/python/PyRuntime.java
@@ -14,6 +14,7 @@
 
 package com.google.devtools.build.lib.rules.python;
 
+import com.google.common.base.Preconditions;
 import com.google.devtools.build.lib.actions.Artifact;
 import com.google.devtools.build.lib.actions.MutableActionGraph.ActionConflictException;
 import com.google.devtools.build.lib.analysis.ConfiguredTarget;
@@ -37,6 +38,9 @@
     Artifact interpreter = ruleContext.getPrerequisiteArtifact("interpreter", Mode.TARGET);
     PathFragment interpreterPath =
         PathFragment.create(ruleContext.attributes().get("interpreter_path", Type.STRING));
+    PythonVersion pythonVersion =
+        PythonVersion.parseTargetOrSentinelValue(
+            ruleContext.attributes().get("python_version", Type.STRING));
 
     // Determine whether we're pointing to an in-build target (hermetic) or absolute system path
     // (non-hermetic).
@@ -53,14 +57,21 @@
       ruleContext.attributeError("interpreter_path", "must be an absolute path.");
     }
 
+    if (pythonVersion == PythonVersion._INTERNAL_SENTINEL) {
+      // Use the same default as py_binary/py_test would use for their python_version attribute.
+      // (Of course, in our case there's no configuration transition involved.)
+      pythonVersion = ruleContext.getFragment(PythonConfiguration.class).getDefaultPythonVersion();
+    }
+    Preconditions.checkState(pythonVersion.isTargetValue());
+
     if (ruleContext.hasErrors()) {
       return null;
     }
 
     PyRuntimeInfo provider =
         hermetic
-            ? PyRuntimeInfo.createForInBuildRuntime(interpreter, files)
-            : PyRuntimeInfo.createForPlatformRuntime(interpreterPath);
+            ? PyRuntimeInfo.createForInBuildRuntime(interpreter, files, pythonVersion)
+            : PyRuntimeInfo.createForPlatformRuntime(interpreterPath, pythonVersion);
 
     return new RuleConfiguredTargetBuilder(ruleContext)
         .setFilesToBuild(files)
diff --git a/src/main/java/com/google/devtools/build/lib/rules/python/PyRuntimeInfo.java b/src/main/java/com/google/devtools/build/lib/rules/python/PyRuntimeInfo.java
index 4dab2b3..bfa60dc 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/python/PyRuntimeInfo.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/python/PyRuntimeInfo.java
@@ -52,15 +52,19 @@
   @Nullable private final PathFragment interpreterPath;
   @Nullable private final Artifact interpreter;
   @Nullable private final SkylarkNestedSet files;
+  /** Invariant: either PY2 or PY3. */
+  private final PythonVersion pythonVersion;
 
   private PyRuntimeInfo(
       @Nullable Location location,
       @Nullable PathFragment interpreterPath,
       @Nullable Artifact interpreter,
-      @Nullable SkylarkNestedSet files) {
+      @Nullable SkylarkNestedSet files,
+      PythonVersion pythonVersion) {
     super(PROVIDER, location);
     Preconditions.checkArgument((interpreterPath == null) != (interpreter == null));
     Preconditions.checkArgument((interpreter == null) == (files == null));
+    Preconditions.checkArgument(pythonVersion.isTargetValue());
     if (files != null) {
       // Work around #7266 by special-casing the empty set in the type check.
       Preconditions.checkArgument(
@@ -69,22 +73,25 @@
     this.interpreterPath = interpreterPath;
     this.interpreter = interpreter;
     this.files = files;
+    this.pythonVersion = pythonVersion;
   }
 
   /** Constructs an instance from native rule logic (built-in location) for an in-build runtime. */
   public static PyRuntimeInfo createForInBuildRuntime(
-      Artifact interpreter, NestedSet<Artifact> files) {
+      Artifact interpreter, NestedSet<Artifact> files, PythonVersion pythonVersion) {
     return new PyRuntimeInfo(
         /*location=*/ null,
         /*interpreterPath=*/ null,
         interpreter,
-        SkylarkNestedSet.of(Artifact.class, files));
+        SkylarkNestedSet.of(Artifact.class, files),
+        pythonVersion);
   }
 
   /** Constructs an instance from native rule logic (built-in location) for a platform runtime. */
-  public static PyRuntimeInfo createForPlatformRuntime(PathFragment interpreterPath) {
+  public static PyRuntimeInfo createForPlatformRuntime(
+      PathFragment interpreterPath, PythonVersion pythonVersion) {
     return new PyRuntimeInfo(
-        /*location=*/ null, interpreterPath, /*interpreter=*/ null, /*files=*/ null);
+        /*location=*/ null, interpreterPath, /*interpreter=*/ null, /*files=*/ null, pythonVersion);
   }
 
   @Override
@@ -148,6 +155,15 @@
     return files;
   }
 
+  public PythonVersion getPythonVersion() {
+    return pythonVersion;
+  }
+
+  @Override
+  public String getPythonVersionForStarlark() {
+    return pythonVersion.name();
+  }
+
   /** The class of the {@code PyRuntimeInfo} provider type. */
   public static class PyRuntimeInfoProvider extends BuiltinProvider<PyRuntimeInfo>
       implements PyRuntimeInfoApi.PyRuntimeInfoProviderApi {
@@ -158,7 +174,11 @@
 
     @Override
     public PyRuntimeInfo constructor(
-        Object interpreterPathUncast, Object interpreterUncast, Object filesUncast, Location loc)
+        Object interpreterPathUncast,
+        Object interpreterUncast,
+        Object filesUncast,
+        String pythonVersion,
+        Location loc)
         throws EvalException {
       String interpreterPath =
           interpreterPathUncast == NONE ? null : (String) interpreterPathUncast;
@@ -175,15 +195,27 @@
         throw new EvalException(loc, "cannot specify 'files' if 'interpreter_path' is given");
       }
 
+      PythonVersion parsedPythonVersion;
+      try {
+        parsedPythonVersion = PythonVersion.parseTargetValue(pythonVersion);
+      } catch (IllegalArgumentException ex) {
+        throw new EvalException(loc, "illegal value for 'python_version'", ex);
+      }
+
       if (isInBuildRuntime) {
         if (files == null) {
           files =
               SkylarkNestedSet.of(Artifact.class, NestedSetBuilder.emptySet(Order.STABLE_ORDER));
         }
-        return new PyRuntimeInfo(loc, /*interpreterPath=*/ null, interpreter, files);
+        return new PyRuntimeInfo(
+            loc, /*interpreterPath=*/ null, interpreter, files, parsedPythonVersion);
       } else {
         return new PyRuntimeInfo(
-            loc, PathFragment.create(interpreterPath), /*interpreter=*/ null, /*files=*/ null);
+            loc,
+            PathFragment.create(interpreterPath),
+            /*interpreter=*/ null,
+            /*files=*/ null,
+            parsedPythonVersion);
       }
     }
   }
diff --git a/src/main/java/com/google/devtools/build/lib/rules/python/PyRuntimeRule.java b/src/main/java/com/google/devtools/build/lib/rules/python/PyRuntimeRule.java
index 5126ce9..1a92a32 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/python/PyRuntimeRule.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/python/PyRuntimeRule.java
@@ -33,6 +33,9 @@
   public RuleClass build(RuleClass.Builder builder, RuleDefinitionEnvironment env) {
     return builder
 
+        // For --incompatible_py3_is_default.
+        .requiresConfigurationFragments(PythonConfiguration.class)
+
         /* <!-- #BLAZE_RULE(py_runtime).ATTRIBUTE(files) -->
         For an in-build runtime, this is the set of files comprising this runtime. These files will
         be added to the runfiles of Python binaries that use this runtime. For a platform runtime
@@ -51,6 +54,18 @@
         platform. For an in-build runtime this attribute must not be set.
         <!-- #END_BLAZE_RULE.ATTRIBUTE --> */
         .add(attr("interpreter_path", STRING))
+
+        /* <!-- #BLAZE_RULE(py_runtime).ATTRIBUTE(python_version) -->
+        Whether this runtime is for Python major version 2 or 3. Valid values are <code>"PY2"</code>
+        and <code>"PY3"</code>.
+
+        <p>The default value is controlled by the <code>--incompatible_py3_is_default</code> flag.
+        However, in the future this attribute will be mandatory and have no default value.
+        <!-- #END_BLAZE_RULE.ATTRIBUTE --> */
+        .add(
+            attr("python_version", STRING)
+                .value(PythonVersion._INTERNAL_SENTINEL.toString())
+                .allowedValues(PyRuleClasses.TARGET_PYTHON_ATTR_VALUE_SET))
         .add(attr("output_licenses", LICENSE))
         .build();
   }
diff --git a/src/main/java/com/google/devtools/build/lib/rules/python/PythonConfiguration.java b/src/main/java/com/google/devtools/build/lib/rules/python/PythonConfiguration.java
index c8d68d6a..7dcc069 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/python/PythonConfiguration.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/python/PythonConfiguration.java
@@ -39,6 +39,7 @@
 public class PythonConfiguration extends BuildConfiguration.Fragment {
 
   private final PythonVersion version;
+  private final PythonVersion defaultVersion;
   private final TriState buildPythonZip;
   private final boolean buildTransitiveRunfilesTrees;
 
@@ -51,12 +52,14 @@
 
   PythonConfiguration(
       PythonVersion version,
+      PythonVersion defaultVersion,
       TriState buildPythonZip,
       boolean buildTransitiveRunfilesTrees,
       boolean oldPyVersionApiAllowed,
       boolean useNewPyVersionSemantics,
       boolean disallowLegacyPyProvider) {
     this.version = version;
+    this.defaultVersion = defaultVersion;
     this.buildPythonZip = buildPythonZip;
     this.buildTransitiveRunfilesTrees = buildTransitiveRunfilesTrees;
     this.oldPyVersionApiAllowed = oldPyVersionApiAllowed;
@@ -75,6 +78,21 @@
     return version;
   }
 
+  /**
+   * Returns the default Python version to use on targets that omit their {@code python_version}
+   * attribute.
+   *
+   * <p>Specified using {@code --incompatible_py3_is_default}. Long-term, the default will simply be
+   * hardcoded as {@code PY3}.
+   *
+   * <p>This information is stored on the configuration for the benefit of callers in rule analysis.
+   * However, transitions have access to the option fragment instead of the configuration fragment,
+   * and should rely on {@link PythonOptions#getDefaultPythonVersion} instead.
+   */
+  public PythonVersion getDefaultPythonVersion() {
+    return defaultVersion;
+  }
+
   @Override
   public String getOutputDirectoryName() {
     // TODO(brandjon): Implement alternative semantics for controlling which python version(s) get
diff --git a/src/main/java/com/google/devtools/build/lib/rules/python/PythonConfigurationLoader.java b/src/main/java/com/google/devtools/build/lib/rules/python/PythonConfigurationLoader.java
index 08095e7..c5c2fba 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/python/PythonConfigurationLoader.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/python/PythonConfigurationLoader.java
@@ -36,6 +36,7 @@
     PythonVersion pythonVersion = pythonOptions.getPythonVersion();
     return new PythonConfiguration(
         pythonVersion,
+        pythonOptions.getDefaultPythonVersion(),
         pythonOptions.buildPythonZip,
         pythonOptions.buildTransitiveRunfilesTrees,
         /*oldPyVersionApiAllowed=*/ !pythonOptions.incompatibleRemoveOldPythonVersionApi,
diff --git a/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/python/PyRuntimeInfoApi.java b/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/python/PyRuntimeInfoApi.java
index ebd79d1..b1ccaad 100644
--- a/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/python/PyRuntimeInfoApi.java
+++ b/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/python/PyRuntimeInfoApi.java
@@ -78,6 +78,14 @@
   @Nullable
   SkylarkNestedSet getFilesForStarlark();
 
+  @SkylarkCallable(
+      name = "python_version",
+      structField = true,
+      doc =
+          "Indicates whether this runtime uses Python major version 2 or 3. Valid values are "
+              + "(only) <code>\"PY2\"</code> and <code>\"PY3\"</code>.")
+  String getPythonVersionForStarlark();
+
   /** Provider type for {@link PyRuntimeInfoApi} objects. */
   @SkylarkModule(name = "Provider", documented = false, doc = "")
   interface PyRuntimeInfoProviderApi extends ProviderApi {
@@ -119,12 +127,22 @@
                       + "for this argument if you pass in <code>interpreter_path</code>. If "
                       + "<code>interpreter</code> is given and this argument is <code>None</code>, "
                       + "<code>files</code> becomes an empty <code>depset</code> instead."),
+          @Param(
+              name = "python_version",
+              type = String.class,
+              positional = false,
+              named = true,
+              doc = "The value for the new object's <code>python_version</code> field."),
         },
         selfCall = true,
         useLocation = true)
     @SkylarkConstructor(objectType = PyRuntimeInfoApi.class, receiverNameForDoc = "PyRuntimeInfo")
     PyRuntimeInfoApi<?> constructor(
-        Object interpreterPathUncast, Object interpreterUncast, Object filesUncast, Location loc)
+        Object interpreterPathUncast,
+        Object interpreterUncast,
+        Object filesUncast,
+        String pythonVersion,
+        Location loc)
         throws EvalException;
   }
 }
diff --git a/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/python/FakePyRuntimeInfo.java b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/python/FakePyRuntimeInfo.java
index 8ab56ab..7ba31e7 100644
--- a/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/python/FakePyRuntimeInfo.java
+++ b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/python/FakePyRuntimeInfo.java
@@ -40,6 +40,11 @@
   }
 
   @Override
+  public String getPythonVersionForStarlark() {
+    return "";
+  }
+
+  @Override
   public void repr(SkylarkPrinter printer) {}
 
   /** Fake implementation of {@link PyRuntimeInfoProviderApi}. */
@@ -47,7 +52,11 @@
 
     @Override
     public PyRuntimeInfoApi<?> constructor(
-        Object interpreterPathUncast, Object interpreterUncast, Object filesUncast, Location loc)
+        Object interpreterPathUncast,
+        Object interpreterUncast,
+        Object filesUncast,
+        String pythonVersion,
+        Location loc)
         throws EvalException {
       return new FakePyRuntimeInfo();
     }
diff --git a/src/test/java/com/google/devtools/build/lib/rules/python/BUILD b/src/test/java/com/google/devtools/build/lib/rules/python/BUILD
index bd56e2e..e5b1b9e 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/python/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/rules/python/BUILD
@@ -58,6 +58,7 @@
     name = "PyRuntimeTest",
     srcs = ["PyRuntimeTest.java"],
     deps = [
+        ":PythonTestUtils",
         "//src/main/java/com/google/devtools/build/lib:bazel-rules",
         "//src/main/java/com/google/devtools/build/lib:python-rules",
         "//src/test/java/com/google/devtools/build/lib:actions_testutil",
diff --git a/src/test/java/com/google/devtools/build/lib/rules/python/PyRuntimeInfoTest.java b/src/test/java/com/google/devtools/build/lib/rules/python/PyRuntimeInfoTest.java
index 7da0c00..495cd6e 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/python/PyRuntimeInfoTest.java
+++ b/src/test/java/com/google/devtools/build/lib/rules/python/PyRuntimeInfoTest.java
@@ -54,7 +54,8 @@
   @Test
   public void factoryMethod_InBuildRuntime() {
     NestedSet<Artifact> files = NestedSetBuilder.create(Order.STABLE_ORDER, dummyFile);
-    PyRuntimeInfo inBuildRuntime = PyRuntimeInfo.createForInBuildRuntime(dummyInterpreter, files);
+    PyRuntimeInfo inBuildRuntime =
+        PyRuntimeInfo.createForInBuildRuntime(dummyInterpreter, files, PythonVersion.PY2);
 
     assertThat(inBuildRuntime.getCreationLoc()).isEqualTo(Location.BUILTIN);
     assertThat(inBuildRuntime.getInterpreterPath()).isNull();
@@ -62,12 +63,14 @@
     assertThat(inBuildRuntime.getInterpreter()).isEqualTo(dummyInterpreter);
     assertThat(inBuildRuntime.getFiles()).isEqualTo(files);
     assertThat(inBuildRuntime.getFilesForStarlark().getSet(Artifact.class)).isEqualTo(files);
+    assertThat(inBuildRuntime.getPythonVersion()).isEqualTo(PythonVersion.PY2);
+    assertThat(inBuildRuntime.getPythonVersionForStarlark()).isEqualTo("PY2");
   }
 
   @Test
   public void factoryMethod_PlatformRuntime() {
     PathFragment path = PathFragment.create("/system/interpreter");
-    PyRuntimeInfo platformRuntime = PyRuntimeInfo.createForPlatformRuntime(path);
+    PyRuntimeInfo platformRuntime = PyRuntimeInfo.createForPlatformRuntime(path, PythonVersion.PY2);
 
     assertThat(platformRuntime.getCreationLoc()).isEqualTo(Location.BUILTIN);
     assertThat(platformRuntime.getInterpreterPath()).isEqualTo(path);
@@ -75,6 +78,8 @@
     assertThat(platformRuntime.getInterpreter()).isNull();
     assertThat(platformRuntime.getFiles()).isNull();
     assertThat(platformRuntime.getFilesForStarlark()).isNull();
+    assertThat(platformRuntime.getPythonVersion()).isEqualTo(PythonVersion.PY2);
+    assertThat(platformRuntime.getPythonVersionForStarlark()).isEqualTo("PY2");
   }
 
   @Test
@@ -83,12 +88,14 @@
         "info = PyRuntimeInfo(",
         "    interpreter = dummy_interpreter,",
         "    files = depset([dummy_file]),",
+        "    python_version = 'PY2',",
         ")");
     PyRuntimeInfo info = (PyRuntimeInfo) lookup("info");
     assertThat(info.getCreationLoc().getStartOffset()).isEqualTo(7);
     assertThat(info.getInterpreterPath()).isNull();
     assertThat(info.getInterpreter()).isEqualTo(dummyInterpreter);
     assertHasOrderAndContainsExactly(info.getFiles(), Order.STABLE_ORDER, dummyFile);
+    assertThat(info.getPythonVersion()).isEqualTo(PythonVersion.PY2);
   }
 
   @Test
@@ -96,12 +103,14 @@
     eval(
         "info = PyRuntimeInfo(", //
         "    interpreter_path = '/system/interpreter',",
+        "    python_version = 'PY2',",
         ")");
     PyRuntimeInfo info = (PyRuntimeInfo) lookup("info");
     assertThat(info.getCreationLoc().getStartOffset()).isEqualTo(7);
     assertThat(info.getInterpreterPath()).isEqualTo(PathFragment.create("/system/interpreter"));
     assertThat(info.getInterpreter()).isNull();
     assertThat(info.getFiles()).isNull();
+    assertThat(info.getPythonVersion()).isEqualTo(PythonVersion.PY2);
   }
 
   @Test
@@ -109,6 +118,7 @@
     eval(
         "info = PyRuntimeInfo(", //
         "    interpreter = dummy_interpreter,",
+        "    python_version = 'PY2',",
         ")");
     PyRuntimeInfo info = (PyRuntimeInfo) lookup("info");
     assertHasOrderAndContainsExactly(info.getFiles(), Order.STABLE_ORDER);
@@ -118,12 +128,15 @@
   public void starlarkConstructorErrors_InBuildXorPlatform() throws Exception {
     checkEvalErrorContains(
         "exactly one of the 'interpreter' or 'interpreter_path' arguments must be specified",
-        "PyRuntimeInfo()");
+        "PyRuntimeInfo(",
+        "    python_version = 'PY2',",
+        ")");
     checkEvalErrorContains(
         "exactly one of the 'interpreter' or 'interpreter_path' arguments must be specified",
         "PyRuntimeInfo(",
         "    interpreter_path = '/system/interpreter',",
         "    interpreter = dummy_interpreter,",
+        "    python_version = 'PY2',",
         ")");
   }
 
@@ -134,18 +147,37 @@
         "PyRuntimeInfo(",
         "    interpreter = dummy_interpreter,",
         "    files = 'abc',",
+        "    python_version = 'PY2',",
         ")");
     checkEvalErrorContains(
         "expected value of type 'depset of Files or NoneType' for parameter 'files'",
         "PyRuntimeInfo(",
         "    interpreter = dummy_interpreter,",
         "    files = depset(['abc']),",
+        "    python_version = 'PY2',",
         ")");
     checkEvalErrorContains(
         "cannot specify 'files' if 'interpreter_path' is given",
         "PyRuntimeInfo(",
         "    interpreter_path = '/system/interpreter',",
         "    files = depset([dummy_file]),",
+        "    python_version = 'PY2',",
+        ")");
+  }
+
+  @Test
+  public void starlarkConstructorErrors_PythonVersion() throws Exception {
+    checkEvalErrorContains(
+        "parameter 'python_version' has no default value",
+        "PyRuntimeInfo(",
+        "    interpreter_path = '/system/interpreter',",
+        ")");
+    checkEvalErrorContains(
+        "illegal value for 'python_version': 'not a Python version' is not a valid Python major "
+            + "version. Expected 'PY2' or 'PY3'.",
+        "PyRuntimeInfo(",
+        "    interpreter_path = '/system/interpreter',",
+        "    python_version = 'not a Python version',",
         ")");
   }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/rules/python/PyRuntimeTest.java b/src/test/java/com/google/devtools/build/lib/rules/python/PyRuntimeTest.java
index 3c528f5..89a2293 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/python/PyRuntimeTest.java
+++ b/src/test/java/com/google/devtools/build/lib/rules/python/PyRuntimeTest.java
@@ -15,6 +15,7 @@
 package com.google.devtools.build.lib.rules.python;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.devtools.build.lib.rules.python.PythonTestUtils.ensureDefaultIsPY2;
 
 import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
 import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
@@ -40,6 +41,7 @@
         "    name = 'myruntime',",
         "    files = [':myfile'],",
         "    interpreter = ':myinterpreter',",
+        "    python_version = 'PY2',",
         ")");
     PyRuntimeInfo info = getConfiguredTarget("//pkg:myruntime").get(PyRuntimeInfo.PROVIDER);
 
@@ -47,6 +49,7 @@
     assertThat(info.getInterpreterPath()).isNull();
     assertThat(info.getInterpreter().getExecPathString()).isEqualTo("pkg/myinterpreter");
     assertThat(ActionsTestUtil.baseArtifactNames(info.getFiles())).containsExactly("myfile");
+    assertThat(info.getPythonVersion()).isEqualTo(PythonVersion.PY2);
   }
 
   @Test
@@ -56,6 +59,7 @@
         "py_runtime(",
         "    name = 'myruntime',",
         "    interpreter_path = '/system/interpreter',",
+        "    python_version = 'PY2',",
         ")");
     PyRuntimeInfo info = getConfiguredTarget("//pkg:myruntime").get(PyRuntimeInfo.PROVIDER);
 
@@ -63,6 +67,30 @@
     assertThat(info.getInterpreterPath().getPathString()).isEqualTo("/system/interpreter");
     assertThat(info.getInterpreter()).isNull();
     assertThat(info.getFiles()).isNull();
+    assertThat(info.getPythonVersion()).isEqualTo(PythonVersion.PY2);
+  }
+
+  @Test
+  public void pythonVersionDefault() throws Exception {
+    ensureDefaultIsPY2();
+    scratch.file(
+        "pkg/BUILD",
+        "py_runtime(",
+        "    name = 'myruntime_default',",
+        "    interpreter_path = '/system/interpreter',",
+        ")",
+        "py_runtime(",
+        "    name = 'myruntime_explicit',",
+        "    interpreter_path = '/system/interpreter',",
+        "    python_version = 'PY3',",
+        ")");
+    PyRuntimeInfo infoDefault =
+        getConfiguredTarget("//pkg:myruntime_default").get(PyRuntimeInfo.PROVIDER);
+    PyRuntimeInfo infoExplicit =
+        getConfiguredTarget("//pkg:myruntime_explicit").get(PyRuntimeInfo.PROVIDER);
+
+    assertThat(infoDefault.getPythonVersion()).isEqualTo(PythonVersion.PY2);
+    assertThat(infoExplicit.getPythonVersion()).isEqualTo(PythonVersion.PY3);
   }
 
   @Test
@@ -74,6 +102,7 @@
         "    name = 'myruntime',",
         "    interpreter = ':myinterpreter',",
         "    interpreter_path = '/system/interpreter',",
+        "    python_version = 'PY2',",
         ")");
     getConfiguredTarget("//pkg:myruntime");
 
@@ -88,6 +117,7 @@
         "pkg/BUILD", //
         "py_runtime(",
         "    name = 'myruntime',",
+        "    python_version = 'PY2',",
         ")");
     getConfiguredTarget("//pkg:myruntime");
 
@@ -103,6 +133,7 @@
         "py_runtime(",
         "    name = 'myruntime',",
         "    interpreter_path = 'some/relative/path',",
+        "    python_version = 'PY2',",
         ")");
     getConfiguredTarget("//pkg:myruntime");
 
@@ -118,9 +149,25 @@
         "    name = 'myruntime',",
         "    files = [':myfile'],",
         "    interpreter_path = '/system/interpreter',",
+        "    python_version = 'PY2',",
         ")");
     getConfiguredTarget("//pkg:myruntime");
 
     assertContainsEvent("if 'interpreter_path' is given then 'files' must be empty");
   }
+
+  @Test
+  public void badPythonVersionAttribute() throws Exception {
+    reporter.removeHandler(failFastHandler);
+    scratch.file(
+        "pkg/BUILD",
+        "py_runtime(",
+        "    name = 'myruntime',",
+        "    interpreter_path = '/system/interpreter',",
+        "    python_version = 'not a Python version',",
+        ")");
+    getConfiguredTarget("//pkg:myruntime");
+
+    assertContainsEvent("invalid value in 'python_version' attribute");
+  }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/rules/python/PythonStarlarkApiTest.java b/src/test/java/com/google/devtools/build/lib/rules/python/PythonStarlarkApiTest.java
index c9cf589..89f7da4 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/python/PythonStarlarkApiTest.java
+++ b/src/test/java/com/google/devtools/build/lib/rules/python/PythonStarlarkApiTest.java
@@ -140,7 +140,8 @@
         "    info = ctx.attr.runtime[PyRuntimeInfo]",
         "    return [PyRuntimeInfo(",
         "        interpreter = ctx.file.interpreter,",
-        "        files = depset(direct = ctx.files.files, transitive=[info.files]))]",
+        "        files = depset(direct = ctx.files.files, transitive=[info.files]),",
+        "        python_version = info.python_version)]",
         "",
         "userruntime = rule(",
         "    implementation = _userruntime_impl,",
@@ -157,6 +158,7 @@
         "    name = 'pyruntime',",
         "    interpreter = ':intr',",
         "    files = ['data.txt'],",
+        "    python_version = 'PY2',",
         ")",
         "userruntime(",
         "    name = 'userruntime',",