Safeguard
Open Source Security•

Python 3.10 End of Life: What Stopped on 1 October 2026 and How Fast Your Dependencies Will Leave

Python 3.10 reached end of life on 1 October 2026. Its final release, 3.10.22, shipped that day with fixes for eight CVEs, and the CPython team will not fix anything in 3.10 again. The interpreter keeps running.

Safeguard Security Team
Security Research
15 min read

Python 3.10 reached end of life on 1 October 2026. Its final release, 3.10.22, shipped that day with fixes for eight CVEs, and the CPython team will not fix anything in 3.10 again. The interpreter keeps running. What changes is that every future CPython vulnerability stays open on 3.10, and your PyPI dependencies start leaving it one release at a time.

This post is for teams that still run 3.10 in services, containers, CI or Lambda functions. It covers what stopped on 1 October, who still patches 3.10 and until when, how quickly the ecosystem drops an end-of-life Python (with data from the 3.9 end of life a year ago), a script that finds every 3.10 pin in a repository, and the changes that bite when you upgrade.

The short answer

Python 3.10 is end of life as of 1 October 2026. 3.10.22 is its final release and is source-only, so python.org has published no 3.10 installers since 3.10.11. Python 3.11 is now the oldest supported version and receives security fixes until October 2027. 3.12 and 3.13 are also in security-only maintenance, 3.14 is the current bugfix release, and 3.15.0 came out on 9 October 2026. If you are choosing an upgrade target today, pick 3.12 or newer so you aren't doing this again in twelve months.

VersionStatus (python.org, 14 Oct 2026)End of life
3.10End of life1 Oct 2026
3.11Security fixes onlyOct 2027
3.12Security fixes onlyOct 2028
3.13Security fixes onlyOct 2029
3.14BugfixOct 2030
3.15Bugfix (released 9 Oct 2026)Oct 2031

What the final release fixed

The 3.10.22 release notes list eight CVEs, which is a good picture of what future fixes will look like now that 3.10 won't get them:

  • Three tarfile extraction-filter bypasses (CVE-2026-87910, CVE-2026-82049, CVE-2026-19672). These cover a link falling back to extracting a rejected member, hard links to symlinks reaching outside the destination, and paths that leave the destination and come back.
  • Unbounded memory use when zipfile decompresses small bzip2 and LZMA members (CVE-2026-15310).
  • Two ssl issues: SSLContext.wrap_bio() not validating its arguments (CVE-2026-19553), and a crash when an SNI callback switches contexts (CVE-2026-19445).
  • stringprep and the IDNA codec accepting code points outside RFC 3454 (CVE-2026-17084).
  • urllib.request sending HTTPS credentials to matching HTTP URLs (CVE-2026-15806).

There is also a bundled libexpat update to 2.8.5 and hash-flooding hardening for the XML parsers. Archive extraction, TLS and URL handling are exactly the standard-library areas that keep producing CVEs. From now on, fixes in those areas land in 3.11 and later only.

Who still patches 3.10, and until when

Upstream end of life isn't the same as unpatched everywhere. Where your 3.10 comes from decides how exposed you are.

Ubuntu 22.04 LTS. The system python3 on Jammy is 3.10, and Canonical patches it under Ubuntu's own lifecycle, not python.org's. Its latest security upload, 3.10.12-1~22.04.18, was published on 8 September 2026. If your services use the distribution's interpreter on a supported Ubuntu release, the interpreter itself is still covered. Your pip-installed dependencies are not, and that's the next section.

AWS Lambda. The python3.10 runtime runs on Amazon Linux 2. AWS's runtime table lists deprecation on 31 October 2026, function creation blocked from 29 July 2027, and updates blocked from 31 August 2027. AWS says the runtime keeps receiving patches for critical and selected important Amazon Linux 2 security issues, plus language runtime patches, until that deprecation date. After it, no more patches.

Docker official images. The docker-library/python repository now builds 3.11 through 3.15 only. The python:3.10 tag on Docker Hub was last updated on 6 October 2026, and with 3.10 out of the build list it won't be rebuilt. Images built FROM python:3.10 from here on get neither interpreter fixes nor Debian base-image fixes through that tag.

python.org builds, pyenv, uv python install and setup-python. These all give you upstream CPython, so 3.10.22 is the end of the line.

How fast your dependencies will leave

The interpreter is half the problem. The other half is that popular libraries drop support for a Python version soon after it reaches end of life. Once a library's latest release won't install on your interpreter, pip quietly resolves to the newest release that does. Security fixes published after that point never reach you, and nothing in a normal pip install tells you so.

We measured this for the 200 most-downloaded PyPI projects (30-day downloads as of 1 October 2026, from the top-pypi-packages dataset), reading each project's release metadata from PyPI and known vulnerabilities from OSV. A project "drops" a Python version when its latest release declares a Requires-Python that excludes it, or ships compiled wheels for other versions with no wheel or source distribution for it.

Two weeks after its end of life, 3.10 looks fine:

Top 200 PyPI projects whose latest release no longer installs on Python 3.10: 18
...of which the newest release that does install has known vulnerabilities fixed only later: 0 projects, 0 vulnerabilities

Eighteen projects have moved on, including numpy, pandas, scipy, scikit-learn, SQLAlchemy and Starlette, but none has a vulnerability that 3.10 users can't get a fix for. Yet.

Python 3.9 reached end of life on 31 October 2025. Here is the same measurement for it, eleven and a half months later:

Top 200 PyPI projects whose latest release no longer installs on Python 3.9: 112
...of which the newest release that does install has known vulnerabilities fixed only later: 25 projects, 78 vulnerabilities
  urllib3            newest for 3.9: 2.6.3      latest: 2.8.0      5 stranded, e.g. CVE-2026-97687, CVE-2026-97688
  requests           newest for 3.9: 2.32.5     latest: 2.34.2     1 stranded, e.g. CVE-2026-25645
  setuptools         newest for 3.9: 82.0.1     latest: 84.0.0     1 stranded, e.g. CVE-2026-59890
  anyio              newest for 3.9: 4.12.1     latest: 4.15.1     2 stranded, e.g. CVE-2026-64847, CVE-2026-63374
  pytest             newest for 3.9: 8.4.2      latest: 9.1.1      1 stranded, e.g. CVE-2025-71176
  python-dotenv      newest for 3.9: 1.2.1      latest: 1.2.4      1 stranded, e.g. CVE-2026-28684
  fsspec             newest for 3.9: 2025.10.0  latest: 2026.9.0   1 stranded, e.g. CVE-2026-104851
  filelock           newest for 3.9: 3.19.1     latest: 4.1.1      2 stranded, e.g. CVE-2026-22701, CVE-2025-68146
  starlette          newest for 3.9: 0.49.3     latest: 1.8.0      5 stranded, e.g. CVE-2026-54283, CVE-2026-48710
  multidict          newest for 3.9: 6.7.1      latest: 7.1.0      1 stranded, e.g. CVE-2026-104874
  aiohttp            newest for 3.9: 3.13.5     latest: 3.14.5     14 stranded, e.g. CVE-2026-54279, CVE-2026-54273
  pillow             newest for 3.9: 11.3.0     latest: 12.3.0     18 stranded, e.g. CVE-2026-55379, CVE-2026-55798
  pip                newest for 3.9: 26.0.1     latest: 26.2.1     4 stranded, e.g. CVE-2026-3219, CVE-2026-6357
  python-multipart   newest for 3.9: 0.0.20     latest: 0.0.32     7 stranded, e.g. CVE-2026-53539, CVE-2026-53538
  pyarrow            newest for 3.9: 21.0.0     latest: 26.0.0     1 stranded, e.g. CVE-2026-25087
  soupsieve          newest for 3.9: 2.8.4      latest: 3.0.3      2 stranded, e.g. CVE-2026-86000, CVE-2026-85999
  httpx2             newest for 3.9: 2.0.0      latest: 2.13.1     3 stranded, e.g. CVE-2026-84382, CVE-2026-84379
  httpcore2          newest for 3.9: 2.0.0      latest: 2.13.1     1 stranded, e.g. CVE-2026-84381
  msgpack            newest for 3.9: 1.1.2      latest: 1.2.3      1 stranded, e.g. CVE-2026-57585
  mako               newest for 3.9: 1.3.12     latest: 1.4.3      1 stranded, e.g. CVE-2026-102991
  orjson             newest for 3.9: 3.11.5     latest: 3.13.0     1 stranded, e.g. CVE-2025-67221
  langchain          newest for 3.9: 0.3.30     latest: 1.4.5      1 stranded, e.g. CVE-2026-55443
  h2                 newest for 3.9: 4.3.0      latest: 4.4.1      1 stranded, e.g. CVE-2026-71554
  hpack              newest for 3.9: 4.1.0      latest: 4.2.0      1 stranded, e.g. CVE-2026-59980
  langchain-core     newest for 3.9: 0.3.86     latest: 1.6.10     2 stranded, e.g. CVE-2026-26013, CVE-2026-34070

A year after end of life, more than half of the top 200 projects had dropped 3.9, and 25 of them leave 3.9 users on a release with known vulnerabilities that are fixed only in releases they can't install. Several are on the request path of almost every Python web service. urllib3's last release for 3.9 is 2.6.3. Five CVEs in it are fixed only in 2.7 and later, which require Python 3.10. Three of them were published on 30 September 2026, eleven months after 3.9 left support, and every 3.9 environment that resolves urllib3 today is still exposed to them. aiohttp, Pillow, Starlette and python-multipart follow the same pattern.

That is the realistic shape of the next year for 3.10. The count of stranded fixes starts at zero and climbs every time a project that has already dropped 3.10 publishes a security release.

A few limits on these numbers. A distribution may backport a fix into its own package of a library, so a Debian or Ubuntu python3-urllib3 can be safer than the PyPI version with the same number. OSV is only as complete as its sources, and some projects may still publish fixes on an older branch in future. The top 200 skews towards infrastructure libraries, which are maintained more actively than average.

The script is below. Pass it a different version to see where 3.11 stands, or raise the count to cover more of the index.

#!/usr/bin/env python3
"""For the most-downloaded PyPI projects: has the latest release dropped a given
Python version? If so, what is the newest release that still installs on it,
and which known vulnerabilities in that release are fixed only in releases
that no longer support it?  Needs: pip install packaging
Data: https://hugovk.github.io/top-pypi-packages/top-pypi-packages-30-days.min.json
Usage: python3 stranded.py top-pypi-packages.json 3.10 [N=200]"""
import json, sys, urllib.request
from concurrent.futures import ThreadPoolExecutor
from packaging.specifiers import SpecifierSet, InvalidSpecifier
from packaging.version import Version, InvalidVersion

top, pyver = sys.argv[1], sys.argv[2]
N = int(sys.argv[3]) if len(sys.argv) > 3 else 200
PY = Version(pyver + ".99")                      # any patch release of that minor
TAG = "-cp" + pyver.replace(".", "") + "-"       # e.g. -cp310-

def get(url, body=None):
    req = urllib.request.Request(url, data=body, headers={
        "User-Agent": "stranded-check", "Content-Type": "application/json"})
    with urllib.request.urlopen(req, timeout=60) as r:
        return json.load(r)

def installs_on(files):
    files = [f for f in files if not f.get("yanked")]
    if not files:
        return False
    try:
        rp = files[0].get("requires_python") or ""
        if rp and PY not in SpecifierSet(rp):
            return False
    except InvalidSpecifier:
        return False
    native = [f["filename"] for f in files if "-cp3" in f["filename"] and "abi3" not in f["filename"]]
    has_sdist = any(f["packagetype"] == "sdist" for f in files)
    return has_sdist or not native or any(TAG in w for w in native)

def check(name):
    meta = get(f"https://pypi.org/pypi/{name}/json")
    latest = meta["info"]["version"]
    if installs_on(meta["releases"].get(latest, [])):
        return None
    rels = []
    for v, files in meta["releases"].items():
        try:
            if not Version(v).is_prerelease:
                rels.append((Version(v), v, files))
        except InvalidVersion:
            pass
    last = next((v for pv, v, f in sorted(rels, reverse=True) if installs_on(f)), None)
    stranded = []
    if last:
        q = get("https://api.osv.dev/v1/query", json.dumps(
            {"package": {"name": name, "ecosystem": "PyPI"}, "version": last}).encode())
        seen = set()
        for v in q.get("vulns", []):
            ids = {v["id"], *v.get("aliases", [])}
            if ids & seen:
                continue                                  # same issue under another ID
            seen |= ids
            fixed = [e["fixed"] for a in v.get("affected", [])
                     for r in a.get("ranges", []) for e in r.get("events", []) if "fixed" in e]
            if any(Version(f) > Version(last) for f in fixed if f[0].isdigit()):
                stranded.append(next((i for i in sorted(ids) if i.startswith("CVE-")), v["id"]))
    return {"name": name, "latest": latest, "last": last, "stranded": stranded}

names = [r["project"] for r in json.load(open(top))["rows"][:N]]
with ThreadPoolExecutor(12) as ex:
    dropped = [r for r in ex.map(check, names) if r]
hit = [r for r in dropped if r["stranded"]]
print(f"Top {N} PyPI projects whose latest release no longer installs on Python {pyver}: {len(dropped)}")
print(f"...of which the newest release that does install has known vulnerabilities fixed only later: "
      f"{len(hit)} projects, {sum(len(r['stranded']) for r in hit)} vulnerabilities")
for r in hit:
    print(f"  {r['name']:<18} newest for {pyver}: {r['last']:<10} latest: {r['latest']:<10} "
          f"{len(r['stranded'])} stranded, e.g. {', '.join(r['stranded'][:2])}")

Find every 3.10 pin in a repository

Python versions hide in more places than pyproject.toml. This script walks a repository and reports 3.10 or older in packaging metadata, .python-version and runtime.txt, Dockerfile base images, CI workflow matrices, tox and nox environments, and Lambda runtimes in SAM, Serverless, CloudFormation and Terraform files. It uses only the standard library, and exits non-zero when it finds something, so it can run as a CI check.

#!/usr/bin/env python3
"""Find places in a repository that pin an end-of-life Python (3.10 or older by
default): pyproject/setup.cfg, .python-version, runtime.txt, Dockerfiles, CI
workflows, tox, and AWS Lambda/SAM/Serverless/Terraform runtimes.
Usage: python3 py_eol_scan.py [repo_dir] [--max-eol 3.10]"""
import re, sys, pathlib

args = sys.argv[1:]
max_eol = (3, 10)
if "--max-eol" in args:
    i = args.index("--max-eol"); max_eol = tuple(map(int, args[i + 1].split("."))); del args[i:i + 2]
root = pathlib.Path(args[0] if args else ".")
SKIP = {".git", "node_modules", ".venv", "venv", ".tox", "__pycache__", "site-packages"}

PATTERNS = [  # (file glob test, regex with a (major).(minor) group pair, label)
    (lambda p: p.name in ("pyproject.toml", "setup.cfg", "setup.py"),
     r"(?:requires-python|python_requires)\s*=\s*['\"]?[^'\"\n]*?>=\s*(\d)\.(\d+)", "minimum supported Python"),
    (lambda p: p.name in (".python-version", "runtime.txt"),
     r"^(?:python-)?(\d)\.(\d+)", "pinned interpreter"),
    (lambda p: p.name.startswith("Dockerfile") or p.suffix == ".dockerfile",
     r"^\s*FROM\s+(?:\S*/)?python:(\d)\.(\d+)", "container base image"),
    (lambda p: p.suffix in (".yml", ".yaml") and (".github" in p.parts or ".gitlab-ci" in p.name or "azure-pipelines" in p.name),
     r"(?<![\d.])(3)\.(\d+)(?![\d])", "CI matrix / setup-python"),
    (lambda p: p.name in ("tox.ini", "noxfile.py"),
     r"py(3)(\d{2})\b", "tox/nox environment"),
    (lambda p: p.suffix in (".yml", ".yaml", ".json", ".tf"),
     r"(?:Runtime|runtime)\s*[:=]\s*['\"]?python(\d)\.(\d+)", "AWS Lambda runtime"),
]

hits = 0
for path in sorted(root.rglob("*")):
    if not path.is_file() or SKIP & set(path.parts):
        continue
    for test, rx, label in PATTERNS:
        if not test(path):
            continue
        try:
            text = path.read_text(errors="ignore")
        except OSError:
            continue
        for n, line in enumerate(text.splitlines(), 1):
            if label.startswith("CI") and not re.search(r"python(-version)?\s*:", line, re.I):
                continue
            for m in re.finditer(rx, line, re.I):
                ver = (int(m.group(1)), int(m.group(2)))
                if ver[0] == 3 and ver <= max_eol:
                    hits += 1
                    print(f"{path.relative_to(root)}:{n}: Python {ver[0]}.{ver[1]} ({label})  {line.strip()[:80]}")
print(f"\n{hits} reference(s) to end-of-life Python (<= {max_eol[0]}.{max_eol[1]})")
sys.exit(1 if hits else 0)

We ran it against a test repository with a service, a SAM template and CI configuration:

$ python3 py_eol_scan.py fx
fn/template.yaml:5: Python 3.10 (AWS Lambda runtime)  Runtime: python3.10
svc-a/Dockerfile:1: Python 3.10 (container base image)  FROM python:3.10-slim
svc-a/pyproject.toml:3: Python 3.10 (minimum supported Python)  requires-python = ">=3.10"
svc-b/.github/workflows/ci.yml:5: Python 3.10 (CI matrix / setup-python)  python-version: ["3.10", "3.11", "3.12"]
svc-b/.github/workflows/old.yml:5: Python 3.9 (CI matrix / setup-python)  python-version: ["3.12", "3.9", "3.10"]
svc-b/.github/workflows/old.yml:5: Python 3.10 (CI matrix / setup-python)  python-version: ["3.12", "3.9", "3.10"]
svc-b/tox.ini:2: Python 3.10 (tox/nox environment)  envlist = py310, py312

7 reference(s) to end-of-life Python (<= 3.10)

Two notes on reading the results. A requires-python = ">=3.10" line in a library you publish isn't a problem in itself. It means you still claim to support 3.10, which you can keep doing as long as your CI tests it. In an application, that line usually means nobody has bumped it. And --max-eol 3.9 narrows the scan to versions that have been out of support for a year.

Upgrade traps between 3.10 and 3.12+

Most code moves from 3.10 to 3.12 without changes. The common breakages are removals and one TLS change from the final 3.10 release:

  • distutils is gone in 3.12 (PEP 632). Old setup.py files and build scripts that import it need setuptools or a modern build backend.
  • imp, asynchat and asyncore are removed in 3.12. Use importlib and asyncio.
  • 3.13 removes 19 "dead batteries" (PEP 594), including cgi, crypt, telnetlib, pipes and imghdr. Grep for them before choosing 3.13 as your target.
  • TLS hostnames get stricter. The 3.10.22 notes say a missing server_hostname with check_hostname enabled now raises a DeprecationWarning on 3.10 and 3.11, and a ValueError on 3.13 and later. Code that builds TLS connections through wrap_bio() or asyncio without a hostname will fail after the upgrade, which is the right outcome but needs a fix first.
  • Compiled dependencies need new wheels. Pinned old versions of numpy, pandas or pyarrow may have no wheels for 3.12+, which turns an interpreter upgrade into a dependency upgrade. Do both in the same change, with the lockfile regenerated on the new interpreter.

The counter-argument, and what to do this month

Running 3.10 for a while longer is defensible in one case. On Ubuntu 22.04's system interpreter, with dependencies that come from the distribution rather than PyPI, Canonical's patches cover both. Most services don't work that way. They install from PyPI into a virtualenv or container, which is where the data above says the exposure builds up.

For everyone else, the order of work is short:

  1. Run the scanner across your repositories and list every 3.10 (and older) pin, including Lambda runtimes. The 31 October Lambda deprecation is the nearest hard date.
  2. Move FROM python:3.10 images first, because that tag no longer receives rebuilds.
  3. Upgrade services to 3.12 or newer, fixing the removals above and regenerating lockfiles on the new interpreter.
  4. Until a service moves, treat its Python version as a finding in its own right. A dependency scan of a 3.10 environment checks the versions you have, not the fixes you can no longer install.

If you need to answer "which services, across all our repositories, still pin 3.10?" in one place rather than repository by repository, that's the kind of inventory Safeguard SCA keeps. The scripts above will get you most of the way on their own. For the wider practice of tracking end-of-life components, see our guide to managing end-of-life software dependencies.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.