Drop Python 3.9 support

Python 3.9 reached its official End-of-Life (EOL) on October 31, 2025
https://devguide.python.org/versions/

PiperOrigin-RevId: 854341311
diff --git a/.github/workflows/test_python.yml b/.github/workflows/test_python.yml
index f071bbc..83bc307 100644
--- a/.github/workflows/test_python.yml
+++ b/.github/workflows/test_python.yml
@@ -28,7 +28,7 @@
       fail-fast: false   # Don't cancel all jobs if one fails.
       matrix:
         type: [ Pure, C++]
-        version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
+        version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
         include:
           - type: Pure
             targets: //python/... //python:python_version_test
@@ -37,10 +37,6 @@
             targets: //python/... //python:python_version_test
             flags: --define=use_fast_cpp_protos=true
             # Test using WORKSPACE with our oldest support Python version.
-          - version: "3.9"
-            nobzlmod: true
-            # Our 3.9 image has non-hermetic issues and can't be rebuilt.  Use the old version.
-            image: us-docker.pkg.dev/protobuf-build/containers/test/linux/python:7.6.1-3.9-12e21b8dda91028bc14212a3ab582c7c4d149fac
           - version: "3.10"
           - version: "3.11"
             continuous-only: true
diff --git a/.github/workflows/test_upb.yml b/.github/workflows/test_upb.yml
index d5e6b4c..220bb0c 100644
--- a/.github/workflows/test_upb.yml
+++ b/.github/workflows/test_upb.yml
@@ -142,25 +142,21 @@
           # a single wheel. As a result we can just test the oldest and newest
           # supported Python versions and assume this gives us sufficient test
           # coverage.
-          - { os: ubuntu-latest, python-version: "3.9", architecture: x64, type: 'binary' }
-          - { os: macos-14, python-version: "3.9", architecture: arm64, type: 'binary' }
-          - { os: ubuntu-latest, python-version: "3.13", architecture: x64, type: 'binary' }
-          - { os: macos-14, python-version: "3.13", architecture: arm64, type: 'binary' }
-          - { os: ubuntu-latest, python-version: "3.9", architecture: x64, type: 'source'}
-          - { os: macos-14, python-version: "3.9", architecture: arm64, type: 'source', continuous-only: true }
-          - { os: ubuntu-latest, python-version: "3.13", architecture: x64, type: 'source'}
-          - { os: macos-14, python-version: "3.13", architecture: arm64, type: 'source', continuous-only: true }
+          - { os: ubuntu-latest, python-version: "3.10", architecture: x64, type: 'binary' }
+          - { os: macos-14, python-version: "3.10", architecture: arm64, type: 'binary' }
+          - { os: ubuntu-latest, python-version: "3.14", architecture: x64, type: 'binary' }
+          - { os: macos-14, python-version: "3.14", architecture: arm64, type: 'binary' }
+          - { os: ubuntu-latest, python-version: "3.10", architecture: x64, type: 'source'}
+          - { os: macos-14, python-version: "3.10", architecture: arm64, type: 'source', continuous-only: true }
           - { os: ubuntu-latest, python-version: "3.14", architecture: x64, type: 'source', continuous-only: true }
           - { os: macos-14, python-version: "3.14", architecture: arm64, type: 'source', continuous-only: true }
 
           # Windows uses the full API up until Python 3.10.
-          - { os: windows-2022, python-version: "3.9", architecture: x86, type: 'binary', continuous-only: true }
           - { os: windows-2022, python-version: "3.10", architecture: x86, type: 'binary', continuous-only: true }
           - { os: windows-2022, python-version: "3.11", architecture: x86, type: 'binary', continuous-only: true }
           - { os: windows-2022, python-version: "3.12", architecture: x86, type: 'binary', continuous-only: true }
           - { os: windows-2022, python-version: "3.13", architecture: x86, type: 'binary', continuous-only: true }
           - { os: windows-2022, python-version: "3.14", architecture: x86, type: 'binary', continuous-only: true }
-          - { os: windows-2022, python-version: "3.9", architecture: x64, type: 'binary' }
           - { os: windows-2022, python-version: "3.10", architecture: x64, type: 'binary', continuous-only: true }
           - { os: windows-2022, python-version: "3.11", architecture: x64, type: 'binary', continuous-only: true }
           - { os: windows-2022, python-version: "3.12", architecture: x64, type: 'binary', continuous-only: true }
@@ -237,7 +233,7 @@
     strategy:
       fail-fast: false   # Don't cancel all jobs if one fails.
       matrix:
-        python-version: ["3.9", "3.13", "3.14"]
+        python-version: ["3.10", "3.14"]  # oldest + newest versions
     runs-on: ubuntu-latest
     if: ${{ github.event_name != 'pull_request_target' }}
     steps:
diff --git a/MODULE.bazel b/MODULE.bazel
index d60a293..59a9c3b 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -139,11 +139,11 @@
 register_toolchains("//bazel/private/toolchains:all")
 
 SUPPORTED_PYTHON_VERSIONS = [
-    "3.9",
     "3.10",
     "3.11",
     "3.12",
     "3.13",
+    "3.14",
 ]
 
 # TODO: Replace system_python with hermetic_python.
@@ -331,18 +331,8 @@
 # Python headers for release
 python_headers = use_extension("//python/dist:python_downloads.bzl", "python_headers", dev_dependency = True)
 python_headers.source_archive(
-    sha256 = "df796b2dc8ef085edae2597a41c1c0a63625ebd92487adaef2fed22b567873e8",
-    version = "3.9.0",
-)
-python_headers.nuget_package(
-    cpu = "i686",
-    sha256 = "229abecbe49dc08fe5709e0b31e70edfb3b88f23335ebfc2904c44f940fd59b6",
-    version = "3.9.0",
-)
-python_headers.nuget_package(
-    cpu = "x86-64",
-    sha256 = "6af58a733e7dfbfcdd50d55788134393d6ffe7ab8270effbf724bdb786558832",
-    version = "3.9.0",
+    sha256 = "c4e0cbad57c90690cb813fb4663ef670b4d0f587d8171e2c42bd4c9245bd2758",
+    version = "3.10.0",
 )
 python_headers.nuget_package(
     cpu = "i686",
@@ -357,8 +347,6 @@
 use_repo(
     python_headers,
     "nuget_python_i686_3.10.0",
-    "nuget_python_i686_3.9.0",
     "nuget_python_x86-64_3.10.0",
-    "nuget_python_x86-64_3.9.0",
-    "python-3.9.0",
+    "python-3.10.0",
 )
diff --git a/protobuf_deps.bzl b/protobuf_deps.bzl
index d481463..7a29dd7 100644
--- a/protobuf_deps.bzl
+++ b/protobuf_deps.bzl
@@ -150,7 +150,7 @@
     if not native.existing_rule("system_python"):
         system_python(
             name = "system_python",
-            minimum_python_version = "3.9",
+            minimum_python_version = "3.10",
         )
 
     if not native.existing_rule("rules_jvm_external"):
@@ -204,18 +204,8 @@
 
     # Python Downloads
     python_source_archive(
-        version = "3.9.0",
-        sha256 = "df796b2dc8ef085edae2597a41c1c0a63625ebd92487adaef2fed22b567873e8",
-    )
-    python_nuget_package(
-        version = "3.9.0",
-        cpu = "i686",
-        sha256 = "229abecbe49dc08fe5709e0b31e70edfb3b88f23335ebfc2904c44f940fd59b6",
-    )
-    python_nuget_package(
-        version = "3.9.0",
-        cpu = "x86-64",
-        sha256 = "6af58a733e7dfbfcdd50d55788134393d6ffe7ab8270effbf724bdb786558832",
+        version = "3.10.0",
+        sha256 = "c4e0cbad57c90690cb813fb4663ef670b4d0f587d8171e2c42bd4c9245bd2758",
     )
     python_nuget_package(
         version = "3.10.0",
diff --git a/python/BUILD.bazel b/python/BUILD.bazel
index e8c94ae..291a4cc 100644
--- a/python/BUILD.bazel
+++ b/python/BUILD.bazel
@@ -5,7 +5,6 @@
 # license that can be found in the LICENSE file or at
 # https://developers.google.com/open-source/licenses/bsd
 
-load("@bazel_skylib//lib:selects.bzl", "selects")
 load("@bazel_skylib//rules:common_settings.bzl", "bool_flag", "string_flag")
 load("//python:build_targets.bzl", "build_targets")
 load("//python:py_extension.bzl", "py_extension")
@@ -21,7 +20,6 @@
 )
 
 LIMITED_API_FLAG_SELECT = {
-    ":limited_api_3.9": ["-DPy_LIMITED_API=0x03090000"],
     ":limited_api_3.10": ["-DPy_LIMITED_API=0x030a0000"],
     "//conditions:default": [],
 }
@@ -43,40 +41,14 @@
 )
 
 config_setting(
-    name = "limited_api_3.9",
+    name = "limited_api_3.10",
     flag_values = {
         ":limited_api": "True",
-        ":python_version": "39",
+        ":python_version": "310",
     },
 )
 
 config_setting(
-    name = "full_api_3.9_win32",
-    flag_values = {
-        ":limited_api": "False",
-        ":python_version": "39",
-    },
-    values = {"cpu": "win32"},
-)
-
-config_setting(
-    name = "full_api_3.9_win64",
-    flag_values = {
-        ":limited_api": "False",
-        ":python_version": "39",
-    },
-    values = {"cpu": "win64"},
-)
-
-selects.config_setting_group(
-    name = "full_api_3.9",
-    match_any = [
-        "full_api_3.9_win32",
-        ":full_api_3.9_win64",
-    ],
-)
-
-config_setting(
     name = "limited_api_3.10_win32",
     flag_values = {
         ":limited_api": "True",
@@ -94,14 +66,6 @@
     values = {"cpu": "win64"},
 )
 
-selects.config_setting_group(
-    name = "limited_api_3.10",
-    match_any = [
-        ":limited_api_3.10_win32",
-        ":limited_api_3.10_win64",
-    ],
-)
-
 _message_target_compatible_with = {
     "@platforms//os:windows": ["@platforms//:incompatible"],
     "@system_python//:none": ["@platforms//:incompatible"],
diff --git a/python/dist/BUILD.bazel b/python/dist/BUILD.bazel
index e93790a..14d3f47 100644
--- a/python/dist/BUILD.bazel
+++ b/python/dist/BUILD.bazel
@@ -328,7 +328,6 @@
 py_wheel(
     name = "binary_wheel",
     abi = select({
-        "//python:full_api_3.9": "cp39",
         "//conditions:default": "abi3",
     }),
     author = "protobuf@googlegroups.com",
@@ -336,7 +335,6 @@
     classifiers = [
         "Programming Language :: Python",
         "Programming Language :: Python :: 3",
-        "Programming Language :: Python :: 3.9",
         "Programming Language :: Python :: 3.10",
         "Programming Language :: Python :: 3.11",
         "Programming Language :: Python :: 3.12",
@@ -362,15 +360,13 @@
         ":windows_x86_64": "win_amd64",
         "//conditions:default": "any",
     }),
-    python_requires = ">=3.9",
+    python_requires = ">=3.10",
     # LINT.IfChange(python_tag)
     python_tag = selects.with_or({
-        ("//python:limited_api_3.9", "//python:full_api_3.9"): "cp39",
         "//python:limited_api_3.10": "cp310",
         "//conditions:default": "cp" + SYSTEM_PYTHON_VERSION,
     }),
     # LINT.ThenChange(
-    #    :full_api_version,
     #    :limited_api_wheels,
     # )
     strip_path_prefixes = [
@@ -399,7 +395,6 @@
     classifiers = [
         "Programming Language :: Python",
         "Programming Language :: Python :: 3",
-        "Programming Language :: Python :: 3.9",
         "Programming Language :: Python :: 3.10",
         "Programming Language :: Python :: 3.11",
         "Programming Language :: Python :: 3.12",
@@ -413,7 +408,7 @@
     homepage = "https://developers.google.com/protocol-buffers/",
     license = "3-Clause BSD License",
     platform = "any",
-    python_requires = ">=3.9",
+    python_requires = ">=3.10",
     python_tag = "py3",
     strip_path_prefixes = [
         "python/",
@@ -466,12 +461,6 @@
         "win32",
         "win64",
     ],
-    # Windows needs version-specific wheels until 3.10.
-    # LINT.IfChange(full_api_version)
-    full_api_versions = [
-        "39",
-    ],
-    # LINT.ThenChange(:python_tag)
     # Limited API: these wheels will satisfy any Python version >= the
     # given version.
     #
@@ -482,10 +471,10 @@
     limited_api_wheels = {
         "win32": "310",
         "win64": "310",
-        "linux-x86_64": "39",
-        "linux-aarch_64": "39",
-        "linux-s390_64": "39",
-        "osx-universal2": "39",
+        "linux-x86_64": "310",
+        "linux-aarch_64": "310",
+        "linux-s390_64": "310",
+        "osx-universal2": "310",
     },
     # LINT.ThenChange(:python_tag)
     pure_python_wheel = ":pure_python_wheel",
diff --git a/python/dist/setup.py b/python/dist/setup.py
index e73e1aa..92aa8b6 100755
--- a/python/dist/setup.py
+++ b/python/dist/setup.py
@@ -71,7 +71,6 @@
     classifiers=[
         'Programming Language :: Python',
         'Programming Language :: Python :: 3',
-        'Programming Language :: Python :: 3.9',
         'Programming Language :: Python :: 3.10',
         'Programming Language :: Python :: 3.11',
         'Programming Language :: Python :: 3.12',
@@ -89,5 +88,5 @@
             extra_link_args=extra_link_args,
         )
     ],
-    python_requires='>=3.9',
+    python_requires='>=3.10',
 )
diff --git a/python/dist/system_python.bzl b/python/dist/system_python.bzl
index 2a1c564..16a1a95 100644
--- a/python/dist/system_python.bzl
+++ b/python/dist/system_python.bzl
@@ -268,7 +268,7 @@
     implementation = _system_python_impl,
     local = True,
     attrs = {
-        "minimum_python_version": attr.string(default = "3.9"),
+        "minimum_python_version": attr.string(default = "3.10"),
     },
 )
 
diff --git a/python/protobuf_distutils/setup.py b/python/protobuf_distutils/setup.py
index 61cf99f..00fa76e 100644
--- a/python/protobuf_distutils/setup.py
+++ b/python/protobuf_distutils/setup.py
@@ -30,7 +30,6 @@
         # These Python versions should match the protobuf package:
         'Programming Language :: Python',
         'Programming Language :: Python :: 3',
-        'Programming Language :: Python :: 3.9',
         'Programming Language :: Python :: 3.10',
         'Programming Language :: Python :: 3.11',
         'Programming Language :: Python :: 3.12',
diff --git a/python/py_extension.bzl b/python/py_extension.bzl
index 8603aba..82d8445 100644
--- a/python/py_extension.bzl
+++ b/python/py_extension.bzl
@@ -28,9 +28,7 @@
         linkshared = True,
         linkstatic = True,
         deps = deps + select({
-            "//python:limited_api_3.9": ["@python-3.9.0//:python_headers"],
-            "//python:full_api_3.9_win32": ["@nuget_python_i686_3.9.0//:python_full_api"],
-            "//python:full_api_3.9_win64": ["@nuget_python_x86-64_3.9.0//:python_full_api"],
+            "//python:limited_api_3.10": ["@python-3.10.0//:python_headers"],
             "//python:limited_api_3.10_win32": ["@nuget_python_i686_3.10.0//:python_limited_api"],
             "//python:limited_api_3.10_win64": ["@nuget_python_x86-64_3.10.0//:python_limited_api"],
             "//conditions:default": ["@system_python//:python_headers"],