Python 3.10 End of Life: Find Every python3.10 Before Lambda Deprecates It on October 31

Python 3.10 End of Life: Find Every python3.10 Before Lambda Deprecates It on October 31

Python 3.10 reached end of life on October 1, 2026. The final release, 3.10.22, shipped the same day, and the CPython team will not ship another 3.10 fix. On October 31, 2026, AWS Lambda deprecates the python3.10 runtime. Your functions keep running after that date, but they stop getting security patches and you can no longer edit them in the Lambda console.

Most teams don't have one Python 3.10. They have a dozen of them in different places: a Runtime: python3.10 in a SAM template, a FROM python:3.10-slim in a Dockerfile someone wrote in 2022, a CI matrix, a .python-version, a tox env list, a Lambda layer built against the 3.10 ABI. This guide shows how to find all of them in one pass, which version to move to, and what breaks between 3.10 and 3.13 so the upgrade doesn't turn into a day of ImportModuleErrors.

What Actually Changes, and When

DateWhat happensSource
Oct 1, 2026Python 3.10 end of life, final release 3.10.22devguide.python.org, python.org
Oct 31, 2026Lambda python3.10 deprecated: no more security patches, no console create/updateLambda runtimes page
Jan 6, 2027Heroku removes 3.10 from its Python buildpack; existing apps run but can't buildHeroku changelog
Jul 29, 2027Lambda blocks creating new python3.10 functionsLambda runtimes page
Aug 31, 2027Lambda blocks updating code or config of existing python3.10 functionsLambda runtimes page

There's a gap between deprecation and the blocks. After October 31 you can still create and update python3.10 functions through the AWS CLI, SAM or CloudFormation until those 2027 dates. That gives you time. It doesn't mean the runtime is safe: AWS's docs say deprecated runtimes are provided "as-is" with no security patches.

The rest of the ecosystem is moving too. PyTorch has announced that 2.14 is its last release with 3.10 wheels. Once your dependencies stop publishing 3.10 wheels, staying on 3.10 also means staying on old versions of those dependencies.

Which Version to Move To

For Lambda, the managed runtime table makes the decision simple:

RuntimeOSLambda deprecation
python3.10Amazon Linux 2Oct 31, 2026
python3.11Amazon Linux 2Jun 30, 2027
python3.12Amazon Linux 2023Oct 31, 2028
python3.13Amazon Linux 2023Jun 30, 2029
python3.14Amazon Linux 2023Jun 30, 2029
python3.15Amazon Linux 2023Public preview, GA targeted Nov 2026

Don't move to 3.11. It buys you eight months and it's still on Amazon Linux 2. Go straight to 3.13 or 3.14. Both are supported on Lambda until mid-2029. Pick 3.14 if all your compiled dependencies publish wheels for it; otherwise pick 3.13. Python 3.15 came out on October 9, but on Lambda it's still a preview runtime with no SLA. Don't put production on it yet.

The jump from 3.10 to 3.13 crosses three releases' worth of removals, which is what the next sections are about.

Step 1: Find Every 3.10 in a Repository

grep -r "3.10" matches too much (version strings, IPs, changelogs) and still misses the dangerous one: an unquoted python-version: 3.10 in YAML. A YAML parser reads that as the float 3.1, not 3.10. Here's what PyYAML gives on my machine:

1>>> yaml.safe_load('python-version: 3.10')
2{'python-version': 3.1}
3>>> yaml.safe_load('python-version: "3.10"')
4{'python-version': '3.10'}

So I wrote a small scanner that does two things. It looks for 3.10 version pins in IaC, CI and container files. It also parses every .py file with ast and flags imports of standard-library modules and functions that were removed in 3.11, 3.12 and 3.13. It only uses the standard library and runs on Python 3.9 or newer, so you can run it on the old interpreter before you upgrade.

 1#!/usr/bin/env python3
 2"""Find Python 3.10 pins and stdlib APIs removed in 3.11-3.13. Runs on Python 3.9+."""
 3import ast, re, sys
 4from pathlib import Path
 5
 6PIN_PATTERNS = [
 7    (re.compile(r"python3\.10\b"), "runtime id python3.10 (Lambda/SAM/Terraform/Serverless)"),
 8    (re.compile(r"python:3\.10\b"), "container image tag python:3.10"),
 9    (re.compile(r"PYTHON_3_10\b"), "CDK Runtime.PYTHON_3_10"),
10    (re.compile(r"python-version:\s*\[?.*['\"]3\.10['\"]"), "CI python-version 3.10"),
11    (re.compile(r"python-version:\s*3\.10\b"), "CI python-version 3.10 UNQUOTED (YAML reads it as 3.1)"),
12    (re.compile(r"\bpy310\b"), "tox/ruff/black target py310"),
13    (re.compile(r"^\s*3\.10(\.\d+)?\s*$"), ".python-version / runtime.txt pin"),
14]
15REMOVED_MODULES = {
16    "binhex": "3.11", "asynchat": "3.12", "asyncore": "3.12", "distutils": "3.12",
17    "imp": "3.12", "smtpd": "3.12", "lib2to3": "3.13",
18    **{m: "3.13" for m in ("aifc audioop cgi cgitb chunk crypt imghdr mailcap msilib nis "
19                          "nntplib ossaudiodev pipes sndhdr spwd sunau telnetlib uu xdrlib").split()},
20}
21REMOVED_ATTRS = {("asyncio", "coroutine"): "3.11", ("inspect", "getargspec"): "3.11",
22                 ("inspect", "formatargspec"): "3.11"}
23SKIP_DIRS = {".git", ".venv", "venv", "node_modules", "__pycache__", ".tox", "site-packages"}
24TEXT_SUFFIXES = {".yml", ".yaml", ".tf", ".json", ".toml", ".ini", ".cfg", ".txt", ".ts", ".py"}
25TEXT_NAMES = {"Dockerfile", ".python-version", "runtime.txt"}
26
27def scan_py(path, hits):
28    try:
29        tree = ast.parse(path.read_text(encoding="utf-8", errors="replace"))
30    except SyntaxError as e:
31        hits.append(f"{path}:{e.lineno}: does not parse on this interpreter ({e.msg})")
32        return
33    for node in ast.walk(tree):
34        names = []
35        if isinstance(node, ast.Import):
36            names = [a.name for a in node.names]
37        elif isinstance(node, ast.ImportFrom) and node.module and node.level == 0:
38            names = [node.module]
39            for a in node.names:
40                if (node.module, a.name) in REMOVED_ATTRS:
41                    hits.append(f"{path}:{node.lineno}: {node.module}.{a.name} removed in {REMOVED_ATTRS[(node.module, a.name)]}")
42        elif isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name):
43            key = (node.value.id, node.attr)
44            if key in REMOVED_ATTRS:
45                hits.append(f"{path}:{node.lineno}: {key[0]}.{key[1]} removed in {REMOVED_ATTRS[key]}")
46        for n in names:
47            top = n.split(".")[0]
48            if top in REMOVED_MODULES:
49                hits.append(f"{path}:{node.lineno}: import {n} -> module removed in {REMOVED_MODULES[top]}")
50
51def main(root):
52    hits = []
53    for path in sorted(Path(root).rglob("*")):
54        if not path.is_file() or SKIP_DIRS.intersection(path.parts):
55            continue
56        if path.suffix == ".py":
57            scan_py(path, hits)
58        if path.suffix in TEXT_SUFFIXES or path.name in TEXT_NAMES or path.name.startswith("Dockerfile"):
59            for i, line in enumerate(path.read_text(encoding="utf-8", errors="replace").splitlines(), 1):
60                for rx, why in PIN_PATTERNS:
61                    if rx.search(line) and (why.startswith(".python-version") is (path.name in (".python-version", "runtime.txt"))):
62                        hits.append(f"{path}:{i}: {why}: {line.strip()[:80]}")
63    print("\n".join(hits) if hits else "no Python 3.10 pins or removed stdlib APIs found")
64    return 1 if hits else 0
65
66if __name__ == "__main__":
67    sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "."))

I ran it against a test repo seeded with the usual suspects: a SAM template, a Terraform aws_lambda_function, a Dockerfile on the Lambda 3.10 base image, a GitHub Actions workflow with both a quoted matrix and an unquoted setup-python value, a .python-version, a tox env list, and a legacy module. Output on Python 3.9:

 1$ python3 find_py310.py fixture
 2fixture/.github/workflows/ci.yml:5: CI python-version 3.10: python-version: ["3.10", "3.13"]
 3fixture/.github/workflows/ci.yml:10: CI python-version 3.10 UNQUOTED (YAML reads it as 3.1): python-version: 3.10
 4fixture/.python-version:1: .python-version / runtime.txt pin: 3.10.14
 5fixture/Dockerfile:1: container image tag python:3.10: FROM public.ecr.aws/lambda/python:3.10
 6fixture/app/legacy.py:2: import imp -> module removed in 3.12
 7fixture/app/legacy.py:3: import distutils.util -> module removed in 3.12
 8fixture/app/legacy.py:4: import cgi -> module removed in 3.13
 9fixture/app/legacy.py:6: asyncio.coroutine removed in 3.11
10fixture/app/legacy.py:10: inspect.getargspec removed in 3.11
11fixture/infra/main.tf:2: runtime id python3.10 (Lambda/SAM/Terraform/Serverless): runtime = "python3.10"
12fixture/template.yaml:5: runtime id python3.10 (Lambda/SAM/Terraform/Serverless): Runtime: python3.10
13fixture/tox.ini:2: tox/ruff/black target py310: envlist = py310, py313

All 12 seeded problems were found. A directory with only clean code prints no Python 3.10 pins or removed stdlib APIs found and exits 0. When it finds something it exits 1, so you can drop it into CI as a gate. It's a regex and AST scanner, not a type checker. It won't see importlib.import_module("imp") or a removed API reached through an alias, and the removed-API list covers the common cases, not every removal.

Step 2: Find Every python3.10 Function in AWS

The repo scan tells you what you deploy. The account tells you what's actually running, including functions nobody has in source control anymore. AWS documents this command for each region:

1aws lambda list-functions --function-version ALL --region us-east-1 \
2  --output text --query "Functions[?Runtime=='python3.10'].FunctionArn"

--function-version ALL matters. Without it you only see $LATEST. Published versions keep the runtime they were published with, so an alias pointing at version 7 can still be on python3.10 after you've fixed $LATEST. To cover every region:

1for r in $(aws ec2 describe-regions --query "Regions[].RegionName" --output text); do
2  aws lambda list-functions --function-version ALL --region "$r" --output text \
3    --query "Functions[?Runtime=='python3.10'].FunctionArn"
4done

Two places this misses. Container-image functions have no Runtime field, so they never match this query. AWS also says deprecation notifications aren't sent for container image functions at all. Find those through the Dockerfile scan in Step 1. For a fleet view across accounts, the Trusted Advisor check "AWS Lambda Functions Using Deprecated Runtimes" and AWS Config advanced queries both work, but Config only returns $LATEST.

Step 3: Shake Out Removals Before You Switch

Moving 3.10 to 3.13 means getting through these standard-library removals:

Removed inWhatReplace with
3.11binhex, @asyncio.coroutine, inspect.getargspec, inspect.formatargspecasync def, inspect.signature / getfullargspec
3.12distutils, imp, asynchat, asyncore, smtpdsetuptools / packaging, importlib, asyncio
3.12venv no longer pre-installs setuptoolspip install setuptools if you need pkg_resources
3.1319 "dead batteries" (PEP 594): cgi, cgitb, crypt, telnetlib, pipes, imghdr, nntplib, uu, xdrlib and others, plus lib2to3email.message/multipart for cgi, shlex.quote for pipes, third-party packages for the rest

I checked the 3.11 removals directly. Each of these works on 3.9 and fails on 3.11:

1import binhex                    3.11: ModuleNotFoundError: No module named 'binhex'
2asyncio.coroutine                3.11: AttributeError: module 'asyncio' has no attribute 'coroutine'. Did you mean: 'coroutines'?
3inspect.getargspec               3.11: AttributeError: module 'inspect' has no attribute 'getargspec'. Did you mean: 'getargs'?
4inspect.formatargspec            3.11: AttributeError: module 'inspect' has no attribute 'formatargspec'. Did you mean: 'formatargvalues'?

The 3.12 and 3.13 removals are deprecated before they're removed, so you can catch them early by turning deprecation warnings into errors on an older interpreter:

1$ uv run --python 3.11 --no-project -- python -W error::DeprecationWarning -c "import imp"
2DeprecationWarning: the imp module is deprecated in favour of importlib and slated for removal in Python 3.12; ...
3$ uv run --python 3.11 --no-project -- python -W error::DeprecationWarning -c "import cgi"
4DeprecationWarning: 'cgi' is deprecated and slated for removal in Python 3.13

In practice, run your test suite with -W error::DeprecationWarning (or filterwarnings = error::DeprecationWarning in pytest config) on the current version, then run it again on the target version.

One trap I hit while testing this: import distutils raised no warning on that 3.11 interpreter. The reason is that setuptools was installed in it, and setuptools swaps in its own copy of distutils. The real module is only visible with site-packages disabled:

1$ python3.11 -S -W error::DeprecationWarning -c "import distutils"
2DeprecationWarning: The distutils package is deprecated and slated for removal in Python 3.12. ...

So code that does from distutils.util import strtobool can pass your local tests because setuptools happens to be installed, then fail in a clean 3.12+ environment that doesn't have it. Don't rely on warnings alone for distutils. The AST scan in Step 1 flags it no matter what's installed.

Step 4: Upgrade the Function Itself

For a zip-packaged function, the runtime switch is one command:

1aws lambda update-function-configuration \
2  --function-name orders-api --runtime python3.13

The runtime line is the easy part. Three other things change with it:

  1. Rebuild native dependencies. Wheels with C extensions are built for a specific Python ABI. A package built for cp310 won't import under 3.13. Rebuild the deployment package and every layer for the target:

    1pip install -r requirements.txt --target package/ \
    2  --platform manylinux2014_x86_64 --python-version 3.13 \
    3  --implementation cp --only-binary=:all:
    

    Use manylinux2014_aarch64 for arm64 functions. Update the layer's compatible runtimes when you publish it.

  2. The OS changes from Amazon Linux 2 to Amazon Linux 2023. python3.12 and later run on AL2023 with a newer glibc. For container images, the AL2023 Lambda base images use microdnf (symlinked as dnf), not yum. So RUN yum install -y gcc in a Dockerfile on public.ecr.aws/lambda/python:3.10 has to become RUN dnf install -y gcc on :3.13. The AL2023 images are also minimal, so packages that happened to be present on AL2 may not be there.

  3. Publish a new version and move the alias. Fixing $LATEST doesn't touch published versions. Publish, shift a little traffic with weighted aliases, watch errors, then move the whole alias. After August 31, 2027 you won't be able to roll back to python3.10, so finish testing while rollback is still allowed.

Step 5: Everything That Isn't Lambda

  • Containers: move python:3.10-* to python:3.13-* (or 3.14), rebuild, and re-run your image scanner. The official images rebuild their base layers, so a long-lived 3.10 tag also stops getting OS fixes eventually.
  • GitHub Actions: quote every version (python-version: "3.13") and drop 3.10 from matrices once your supported-version policy allows it.
  • Ubuntu 22.04 hosts: the system python3 there is 3.10. Don't replace it, because apt and system tools depend on it. Canonical keeps patching the distro package through 22.04's standard support (to April 2027) and under Ubuntu Pro ESM after that. Run your application on its own interpreter (a container, or uv python install 3.13) instead of the system one.
  • Heroku: set the version in .python-version before the January 6, 2027 buildpack removal, or builds will start failing.

Best Practices

  • Treat the scanner output as a backlog. Fix the code removals first, then flip the version pins, so a pin change never ships alongside an unknown import error.
  • Upgrade one function or service per change, with a version and alias, not the whole account in one deploy.
  • Add the scanner (or an equivalent) to CI with exit code 1 as a failure, so no new 3.10 pin slips back in.
  • Write down a supported-version policy (for example: "newest two Lambda runtimes") so the 3.11 EOL in October 2027 isn't another fire drill.

Common Mistakes to Avoid

  • Unquoted 3.10 in YAML. It becomes 3.1. setup-python will either fail or install something you didn't ask for. Always quote versions.
  • Only updating $LATEST. Aliases on old published versions keep running 3.10.
  • Forgetting layers. A layer built for 3.10 with compiled code attached to a 3.13 function fails at import time, not at deploy time.
  • Trusting a green local test run when setuptools is installed locally and absent in production (see the distutils trap above).
  • Stopping at 3.11. It's on Amazon Linux 2 and deprecated on Lambda in June 2027.

Troubleshooting

Runtime.ImportModuleError: Unable to import module 'lambda_function': No module named 'distutils': the code or a dependency imports distutils. Upgrade the dependency, or replace the call (strtobool is about five lines to write yourself). Adding setuptools to the package is a stopgap.

No module named 'pydantic_core._pydantic_core' (or any _something compiled module): the package was built for the wrong Python version or architecture. Rebuild it with the pip --platform ... --python-version 3.13 --only-binary=:all: command above.

Container build fails on yum: you're on an AL2023 base image. Use dnf.

You can't edit the function in the console after October 31: that's the deprecation behaviour. The CLI, SAM and CloudFormation still work until August 31, 2027. Use them to switch the runtime.

FAQ

Will my python3.10 Lambda functions stop working on October 31? No. Invocations continue. You lose security patches, technical support and console editing. Creation is blocked from July 29, 2027 and updates from August 31, 2027.

Should I jump straight to Python 3.15? Not on Lambda yet. python3.15 is a public preview runtime with no SLA, and GA is targeted for November 2026. Use 3.13 or 3.14 now.

Is Python 3.10 on Ubuntu 22.04 insecure now? Not immediately. Canonical patches its own python3.10 package for the life of the release. What you lose is upstream fixes for any 3.10 you built or installed yourself (pyenv, Docker python:3.10, Lambda).

How long will 3.13 last? Upstream until October 2029. On Lambda, python3.13 is scheduled for deprecation on June 30, 2029.

Key Takeaways

ItemAction
Upstream 3.10EOL Oct 1, 2026; 3.10.22 was the last release
Lambda python3.10Deprecated Oct 31, 2026; create blocked Jul 29, 2027; update blocked Aug 31, 2027
Targetpython3.13 or python3.14 (AL2023, supported to mid-2029)
Biggest code risksdistutils, imp, asyncio.coroutine, PEP 594 modules like cgi and pipes
Biggest packaging riskNative wheels and layers built for cp310; yum vs dnf in AL2023 images
Easy-to-miss config bugUnquoted 3.10 in YAML is read as 3.1

What I Tested and What I Didn't

The scanner was run locally on Python 3.9 and 3.11 against the seeded test repo shown above, plus a clean directory as the negative case. The 3.11 removal errors, the -W error::DeprecationWarning output, the setuptools distutils masking, and the YAML 3.1 behaviour were all run on this machine. I don't have an AWS account wired up here, so every aws lambda command, the pip --platform packaging command, the AL2023 dnf change and the Lambda dates come from AWS's current documentation and were not run. The 3.12 and 3.13 removal lists come from the official "What's New" pages and were not executed on those interpreters. Check one function end to end in a non-production account before you roll this out.

Further Reading