If you're operating a fleet of AWS Lambda functions and looking at Python runtime upgrades, this is what you're walking into.
The setup
We had a small fleet of Python Lambda functions, most still on python3.9, a few on python3.12. AWS had announced the python3.9 runtime deprecation, and with this announcement long overdue, it was time to upgrade. The plan was straightforward: jump everything to python3.13, the latest supported runtime, at the time, and be done with it for another two years.
For most Lambdas, the change was exactly as advertised. One line in the Terraform file:
runtime = "python3.13"
Apply, redeploy, done. The functions came up cleanly, ran their tests, and went back to processing events. No code changes needed. No imports broken. No surprises.
Then one Lambda refused to start.
The failure
The first invocation after the runtime change came back with:
[ERROR] Runtime.ImportModuleError: Unable to import module 'index':
No module named 'cgi'
This is the kind of error that's easy to misread. "No module named X" usually means you forgot to install something. But cgi is/was part of the Python standard library. It's not a thing you install. It's just there. Until it isn't.
What changed in 3.13
Python 3.13 follows through on PEP 594, the "dead batteries" proposal accepted back in 2019. The PEP marked a long list of legacy standard-library modules as deprecated in 3.11, with removal scheduled for 3.13. The full removal list:
aifc, audioop, cgi, cgitb, chunk, crypt, imghdr,
mailcap, msilib, nis, nntplib, ossaudiodev, pipes,
sndhdr, spwd, sunau, telnetlib, uu, xdrlib
Most of these are CGI-era, dial-up-era, or platform-specific holdovers that nobody touches anymore. Which is exactly why they were removed.
But the broken Lambda wasn't importing cgi directly. Our code only imported boto3, json, and os. The cgi import was coming from somewhere inside the deployment package.
The actual root cause
The Lambda was deployed as a zip that bundled its Python dependencies and those dependencies hadn't been touched in years. Inside the package:
boto31.23.10 (released 2022)botocore1.26.10 (released 2022)Jinja23.0.3 (older)
botocore 1.26.10 has a module called botocore/utils.py that at that version imports cgi. Modern botocore long ago refactored that out, but the version frozen into this Lambda's deployment package was still on the old code path. The Lambda had been running fine on Python 3.9 and 3.12 because both versions still shipped the cgi module. Python 3.13 didn't. The same package that worked yesterday couldn't even reach lambda_handler today.
There was a secondary issue lurking in the same package: botocore 1.26.10 also depends on six internally. Modern botocore dropped six entirely. So even if we'd manually shimmed in a cgi replacement, the next thing waiting to break was the six dependency chain.
The fix
Two lines in requirements.txt:
boto3>=1.35.0
Jinja2>=3.1.0
Rebuild the zip, redeploy. The Lambda came up. Total time from "this should be a one-line change" to "this Lambda is back online": about half a day.
The one-line runtime bump turned into a small dependency audit across every Lambda that bundled its own SDK. We found two more with bundled botocore from 2022 that would have hit the same issue eventually. We updated all of them in the same change set.
What I took from this
Three things;
The Lambda runtime upgrade is one line. The deployment package upgrade is a separate problem. Lambda runtime upgrades are advertised and trivial. The breaking changes are documented, the modules removed are listed, the deprecations are well-flagged. But Lambda runtime upgrades only cover the runtime. The deployment package you ship, your code plus your bundled dependencies has its own version graph that ages independently of the runtime. If a dependency in that package imports something the new runtime no longer ships, the runtime upgrade fails on a problem the runtime didn't cause.
Bundled dependencies age silently. A boto3 version frozen into a Lambda zip in 2022 doesn't get bumped by anything unless you go and bump it. It accumulates known CVEs, deprecated APIs, and obsolete internal imports, all of which lie dormant for years and then surface on the day you change something completely unrelated. The trigger could be a runtime upgrade. It could be a Python interpreter change in a developer's local environment. It could be nothing at all, until one day it's everything. If you bundle, periodically check what you bundled. If you can avoid bundling by using AWS-managed Lambda layers like AWSSDKPandas for the heavy dependencies, or the Lambda runtime's pre-installed boto3 for the SDK itself, your blast radius shrinks.
PEP 594 is not done biting. Python 3.13 removed about 19 modules in one release. They had been deprecated since 3.11 (2022) and the warnings were noisy, but warnings only catch the things people read. A 2022 dependency that imports cgi will not get a deprecation warning until you actually run it on 3.12 and even then, only if your test suite exercises the code path that triggers the import. Static analysis won't catch it. pip install won't catch it. The first time you find out is Lambda's cold start. If you have anything in production using vendored or pinned SDKs from before about 2023, assume one of these removed modules is hiding in your dependency graph and audit accordingly.
The actual fix here was three lines of code. The lesson took longer.