diff --git a/build.gradle.kts b/build.gradle.kts
index 0fd98ad3b05e6ea755f7fc1f4e3268fc0dcaf8b0..815ae6901425813c739b112bc9ff981573634bf3 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -491,12 +491,11 @@ tasks.register("pythonDockerBuildPreCommit") {
 }
 
 tasks.register("pythonLintPreCommit") {
-  // TODO(https://github.com/apache/beam/issues/20209): Find a better way to specify lint and formatter tasks without hardcoding py version.
-  dependsOn(":sdks:python:test-suites:tox:py38:lint")
+  dependsOn(":sdks:python:test-suites:tox:pycommon:linter")
 }
 
 tasks.register("pythonFormatterPreCommit") {
-  dependsOn("sdks:python:test-suites:tox:py38:formatter")
+  dependsOn("sdks:python:test-suites:tox:pycommon:formatter")
 }
 
 tasks.register("python38PostCommit") {
diff --git a/sdks/python/test-suites/tox/py38/build.gradle b/sdks/python/test-suites/tox/py38/build.gradle
index 46388532c1e8fdcc4458a2e0951d15d0f0fcd99f..af1d9d2ce9990649361f377fd0ae6698a6d93fcd 100644
--- a/sdks/python/test-suites/tox/py38/build.gradle
+++ b/sdks/python/test-suites/tox/py38/build.gradle
@@ -26,24 +26,10 @@ applyPythonNature()
 // Required to setup a Python 3 virtualenv and task names.
 pythonVersion = '3.8'
 
-toxTask "formatter", "py3-yapf-check"
-check.dependsOn formatter
-
-// TODO(BEAM-12000): Move tasks that aren't specific to 3.8 to Py 3.9.
 def posargs = project.findProperty("posargs") ?: ""
 
-task lint {}
-check.dependsOn lint
-
-toxTask "lintPy38", "py38-lint", "${posargs}"
-lint.dependsOn lintPy38
-
-toxTask "mypyPy38", "py38-mypy", "${posargs}"
-lint.dependsOn mypyPy38
-
 apply from: "../common.gradle"
 
-
 toxTask "testPy38CloudCoverage", "py38-cloudcoverage", "${posargs}"
 test.dependsOn "testPy38CloudCoverage"
 project.tasks.register("preCommitPyCoverage") {
diff --git a/sdks/python/test-suites/tox/pycommon/build.gradle b/sdks/python/test-suites/tox/pycommon/build.gradle
index 7a44c9eb92f0188de0de7f8cf09264c3693f0381..ccb1163c1c8b7e9ce36babf7cd7b57f664ffc6da 100644
--- a/sdks/python/test-suites/tox/pycommon/build.gradle
+++ b/sdks/python/test-suites/tox/pycommon/build.gradle
@@ -24,8 +24,22 @@
 plugins { id 'org.apache.beam.module' }
 applyPythonNature()
 
-toxTask "docs", "py38-docs"
+def posargs = project.findProperty("posargs") ?: ""
+
+toxTask "docs", "docs"
 assemble.dependsOn docs
 
 task preCommitPyCommon() {
-}
\ No newline at end of file
+}
+
+task linter {}
+check.dependsOn linter
+
+toxTask "formatter", "py3-yapf-check"
+check.dependsOn formatter
+
+toxTask "lint", "lint", "${posargs}"
+linter.dependsOn lint
+
+toxTask "mypy", "mypy", "${posargs}"
+linter.dependsOn mypy
diff --git a/sdks/python/tox.ini b/sdks/python/tox.ini
index cb5246abce3f4c9d1c03d8689f201ba9c36d849b..56485ff1e458678c58d6929f74a8675702143a31 100644
--- a/sdks/python/tox.ini
+++ b/sdks/python/tox.ini
@@ -17,7 +17,7 @@
 
 [tox]
 # new environments will be excluded by default unless explicitly added to envlist.
-envlist = py38,py39,py310,py311,py38-{cloud,docs,lint,mypy,cloudcoverage,dask},py39-{cloud},py310-{cloud,dask},py311-{cloud,dask},whitespacelint
+envlist = py38,py39,py310,py311,py38-{cloud,cloudcoverage,dask},py39-{cloud},py310-{cloud,dask},py311-{cloud,dask},docs,lint,mypy,whitespacelint
 toxworkdir = {toxinidir}/target/{env:ENV_NAME:.tox}
 
 [pycodestyle]
@@ -103,7 +103,7 @@ extras = test,gcp,interactive,dataframe,aws
 commands =
   bash {toxinidir}/scripts/run_pytest.sh {envname} "{posargs}" "--cov-report=xml --cov=. --cov-append"
 
-[testenv:py38-lint]
+[testenv:lint]
 # Don't set TMPDIR to avoid "AF_UNIX path too long" errors in pylint.
 setenv =
 # keep the version of pylint in sync with the 'rev' in .pre-commit-config.yaml
@@ -124,7 +124,7 @@ deps =
 commands =
   time {toxinidir}/scripts/run_whitespacelint.sh
 
-[testenv:py38-mypy]
+[testenv:mypy]
 deps =
   mypy==0.790
   dask==2022.01.0
@@ -137,7 +137,7 @@ commands =
   python setup.py mypy
 
 
-[testenv:py38-docs]
+[testenv:docs]
 extras = test,gcp,docs,interactive,dataframe,dask
 deps =
   Sphinx==1.8.5