PyPI code that breaks on Python 3.12 and 3.13
Scripts and results for this post on GitHub
Python 3.12 removed distutils, imp, asyncore, asynchat and smtpd from the standard library. Python 3.13 removed 20 more, among them cgi, pipes, audioop and telnetlib. Code that still imports them stops with errors such as ModuleNotFoundError: No module named 'distutils'.
py-code.org reads the code in the latest release of 354,763 PyPI projects. We listed every project that imports one of the 25 removed modules, then read each import in context to see whether it would really fail. An import inside try/except ImportError or behind a Python version check is safe, and a plain import imp at the top of a file is not.
The short answer
| Question | Answer | Key number |
|---|---|---|
| How many PyPI projects import a removed module? | 1.1% of the indexed projects | 3,763 of 354,763 |
| How many of those projects have an import that would fail? | 43.1% of them have an unguarded import in their own code | 1,623 projects |
| How many of the 60,000 most downloaded projects have one? | 852 have an unguarded import | 544 of them had no release in two years |
| Which well-known packages fail on import? | nose and boto on 3.12, pydub, coreapi, htmlmin and pgpy on 3.13 | 1.8M and 3.4M monthly downloads for nose and boto |
Verdict: the imports that still fail are concentrated in projects nobody releases any more. Of the 852 widely downloaded projects with an unguarded import, 544 (63.8%) have had no release in two years, and they still get 126 million downloads a month.
Python 3.12 and 3.13 removed 25 standard library modules
Python 3.12 removed distutils (PEP 632) and the imp, asyncore, asynchat and smtpd modules. Python 3.13 removed lib2to3 and the 19 "dead batteries" of PEP 594. The Python 3.13 release notes say the dead batteries were removed "due to their historic, obsolete, or insecure status". Each had been deprecated for at least two releases before it went.
The table lists each module, how many indexed projects import it, and how many have an unguarded import in their own code (outside tests, setup scripts and docs). The last column gives the replacement the release notes name, or a copy of the module on PyPI.
| Module | Removed in | Projects that import it | Unguarded | Replacement |
|---|---|---|---|---|
distutils | 3.12 | 2,565 | 973 | setuptools (still ships distutils), or sysconfig, shutil.which, packaging |
imp | 3.12 | 614 | 246 | importlib; zombie-imp on PyPI |
cgi | 3.13 | 455 | 248 | urllib.parse, email.message; legacy-cgi on PyPI |
audioop | 3.13 | 151 | 52 | audioop-lts on PyPI |
imghdr | 3.13 | 114 | 85 | filetype, puremagic; standard-imghdr on PyPI |
pipes | 3.13 | 107 | 37 | subprocess, shlex.quote |
telnetlib | 3.13 | 85 | 60 | telnetlib3, Exscript; standard-telnetlib on PyPI |
crypt | 3.13 | 67 | 18 | hashlib, bcrypt, argon2-cffi; crypt_r on PyPI |
asyncore | 3.12 | 46 | 28 | asyncio; pyasyncore on PyPI |
lib2to3 | 3.13 | 35 | 27 | none in the standard library |
cgitb | 3.13 | 25 | 20 | legacy-cgi on PyPI |
xdrlib | 3.13 | 21 | 14 | standard-xdrlib on PyPI |
aifc | 3.13 | 15 | 11 | standard-aifc on PyPI |
chunk | 3.13 | 15 | 11 | standard-chunk on PyPI |
uu | 3.13 | 14 | 12 | base64 |
smtpd | 3.12 | 10 | 7 | aiosmtpd on PyPI |
sndhdr | 3.13 | 10 | 9 | filetype, puremagic; standard-sndhdr on PyPI |
msilib | 3.13 | 10 | 8 | none |
asynchat | 3.12 | 9 | 4 | asyncio; pyasynchat on PyPI |
nntplib | 3.13 | 9 | 4 | pynntp; standard-nntplib on PyPI |
spwd | 3.13 | 7 | 0 | python-pam |
mailcap | 3.13 | 6 | 1 | mimetypes |
ossaudiodev | 3.13 | 3 | 0 | pygame |
sunau | 3.13 | 2 | 1 | standard-sunau on PyPI |
No indexed project imports nis.
try/except or a Python version check.How to read the table: "imports it" counts every project whose code imports the module anywhere, including safe, guarded imports and imports in tests. "Unguarded" counts projects where at least one import in their own code would fail: at the top of a file (the file fails to import) or inside a function (the function fails when it runs). A project that declares the matching copy from PyPI, such as legacy-cgi, as a required dependency is not counted as unguarded.
ModuleNotFoundError: No module named 'distutils': 973 projects, but setuptools still covers it
distutils is the module the most projects import: 2,565 of them, and 973 have an unguarded import. But it is the one removal whose error often does not appear. The Python 3.12 release notes say that "the third-party Setuptools package continues to provide distutils". When setuptools is installed, import distutils still works on 3.12 and 3.13.
The error appears when setuptools is missing. Since Python 3.12, python -m venv no longer installs setuptools in new virtual environments, so a fresh venv, a slim Docker image or a uv environment can hit it where an older setup did not. These packages, among others, import distutils when you import them, so they need setuptools installed on 3.12 and later:
docker-pycreds(5.2M downloads a month, last release 2018):import dockerpycredsloadsdistutils.spawnaioredis(1.3M, last release 2021):aioredis.connectionimportsdistutils.versionarsenic(1.2M, last release 2021),python-editor(0.7M, 2019) andGPUtil(0.9M, 2018)
For 890 of the 1,623 projects with an unguarded import, every unguarded import is of distutils, so pip install setuptools is enough for them. The lasting fix is to stop using distutils: shutil.which replaces distutils.spawn.find_executable, packaging.version replaces distutils.version, and sysconfig replaces distutils.sysconfig.
ModuleNotFoundError: No module named 'imp': nose and boto fail on import
imp was the old way to load modules by hand, replaced by importlib. Nothing installs it back by default, so an unguarded import imp fails on every Python from 3.12. 614 projects import it and 246 have an unguarded import. Two of the projects with an unguarded import are still widely installed:
- nose, the test runner that pytest replaced, gets 1.8M downloads a month. Its last release came out in 2015.
import noseloadsnose.importer, which starts withimport imp. - boto, the AWS SDK that boto3 replaced, gets 3.4M downloads a month with a last release in 2018.
import botoloadsboto.plugin, which importsimp.
Key point: import nose and import boto both fail on Python 3.12 and later, yet the two packages still get 5.2 million downloads a month between them.
The copy of imp on PyPI, zombie-imp, brings the module back for code you cannot change. For your own code, importlib.import_module, importlib.util.spec_from_file_location and importlib.reload cover what imp was used for.
No module named 'cgi', 'pipes' or 'audioop': the Python 3.13 removals
Among the 20 modules removed in 3.13, cgi (248 projects with an unguarded import), imghdr (85) and telnetlib (60) have the most projects with an unguarded import. Examples where a plain import of the package fails on 3.13:
coreapi(0.5M downloads a month, last release 2017) andhtmlmin(0.7M, 2017) importcgi. coreapi uses it forcgi.parse_header, and htmlmin'smain.pyimports it without using it.pgpy(0.8M, 2022) importsimghdrinpgpy.constants, whichimport pgpyloads.coloredlogs(21.6M, 2021) andhumanfriendly(26.7M, 2021) importpipesat the top of the modules behind their command-line tools. The libraries still import, but thecoloredlogsandhumanfriendlycommands fail on 3.13.
pydub shows why a guard is not always enough. Its pydub/utils.py guards the import:
try:
import audioop
except ImportError:
import pyaudioop as audioop
On 3.13 the first import fails, and so does the fallback, because no pyaudioop module exists. So import pydub stops with No module named 'pyaudioop'. pydub gets 7.5M downloads a month and its last release came out in 2021. Installing audioop-lts, which provides an audioop module, makes pydub work again. Our count treats pydub's import as guarded, so pydub is not in the 1,623.
Tip: for most of the 3.13 removals, the release notes name a copy on PyPI: legacy-cgi or standard-cgi for cgi, audioop-lts for audioop, and standard-pipes, standard-imghdr, standard-telnetlib and so on for the rest. Adding one to your own requirements fixes a dependency you cannot change.
The most downloaded projects with an unguarded import
Of the 60,000 most downloaded projects, 2,202 import a removed module and 852 have an unguarded import in their own code. In the maintained projects we read by hand, many of these imports sit in code that does not run on a new Python. pip, for example, uses sysconfig by default and imports its distutils code only when a Python distributor configures it to use distutils instead. Several build tools import distutils only in helpers for compiling extensions. So in these maintained projects, an unguarded import often means that one file, or one function, fails, and the rest of the package works.
Projects with no release since November 2024 have not shipped a fix in that time. The ten most downloaded of them are below. As in maintained projects, an unguarded import can sit in a part that most users never load. In the future package, the unguarded imports are in its copies of the email modules and in its futurize and pasteurize conversion tools, which are built on lib2to3. import future itself works.
| Project | Downloads, 30 days | Last release | Unguarded imports of | First unguarded import |
|---|---|---|---|---|
future | 36.2M | 2024 | imghdr, lib2to3, sndhdr, uu | future/backports/email/message.py:13 |
humanfriendly | 26.7M | 2021 | pipes | humanfriendly/cli.py:82 |
coloredlogs | 21.6M | 2021 | pipes | coloredlogs/converter/__init__.py:12 |
docker-pycreds | 5.2M | 2018 | distutils | dockerpycreds/utils.py:1 |
pytzdata | 4.1M | 2020 | distutils | pytzdata/commands/make.py:11 |
boto | 3.4M | 2018 | cgi, imp | boto/plugin.py:40 |
oauth2client | 2.5M | 2018 | cgi | oauth2client/contrib/appengine.py:20 |
Pympler | 2.5M | 2024 | cgi | pympler/util/bottle.py:38 |
nose | 1.8M | 2015 | imp | nose/importer.py:12 |
memory-profiler | 1.6M | 2022 | distutils | memory_profiler.py:941 |
544 projects among the 60,000 most downloaded have an unguarded import and no release in two years. Together they had 126 million downloads in 30 days. Some of these downloads may come from other packages that depend on the stale projects. In that case, the people who run the install may not know the stale projects are there.
Why abandoned packages are a bigger risk now that AI finds bugs
An import of a removed module does not make a package insecure. But it is an easy sign that nobody maintains the package: a maintainer would have seen the deprecation warnings for two releases, and the fix is usually one line. A package with no maintainer also has nobody to fix the next security bug found in it, and bugs are now being found faster.
- AI tools find real bugs. In 2024 Google's Big Sleep agent found an exploitable memory bug in SQLite, which Google called "the first public example of an AI agent finding a previously unknown exploitable memory-safety issue in widely used real-world software" (Project Zero). In DARPA's AI Cyber Challenge final in August 2025, the AI systems found 54 of the 63 planted vulnerabilities, patched 43, and found 18 real ones (DARPA). Anthropic says Claude Opus 4.6 found and validated more than 500 high-severity vulnerabilities in open-source code, and that "many of these projects are maintained by small teams or volunteers" (Anthropic).
- Attackers use the same tools, and exploits come sooner. Anthropic reported a state-sponsored espionage campaign in which AI did 80 to 90% of the work (Anthropic). Mandiant's M-Trends 2026 puts the mean time to exploit at an estimated −7 days: on average, attacks start before the patch is out (Google Cloud).
- Maintainers are already swamped. curl ended its bug bounty in January 2026. Its maintainer says the share of reports that were real fell from over 15% to under 5%, and blames AI-generated reports (Daniel Stenberg). Seth Larson, who triages security reports for CPython, pip, urllib3 and Requests, wrote about the same problem in 2024 (sethmlarson.dev).
- Abandoned packages get hijacked or stay unpatched. In 2022 an attacker re-registered the expired email domain of the
ctxmaintainer, reset the PyPI password and uploaded versions that sent environment variables to a server. The incident report estimates they were downloaded 27,000 times (PSF). pycrypto, whose last release is from 2014, has a heap overflow (CVE-2013-7459) that was fixed in its repository but never released (OSV). PEP 594 itself says themailcapmodule "has CVE-2015-20107 filed against it while having no maintainer to help fix it".
Why: the sources above show bugs being found faster and exploited sooner, by defenders and attackers alike. A fix only reaches users when someone releases it, and an abandoned package has nobody to do that. And unmaintained code is common: Black Duck's 2026 audit found components with no development activity in two years in 93% of the codebases it checked (Black Duck).
So the 544 stale projects above are worth checking for more than Python 3.13 support. If one of them is in your dependencies, look for its maintained successor (pytest for nose, boto3 for boto, and redis-py for aioredis, which was merged into redis-py) or vendor and fix the small part you use.
How to check your own dependencies
The quickest test is to run your tests on Python 3.13 in a fresh virtual environment without setuptools. To see which packages import a removed module without installing anything, use the index:
- On py-code.org, search for a module to see the most downloaded packages that use it: imp, distutils, cgi, pipes, audioop. Each result shows the file and line, so you can see whether the import is guarded.
- From a terminal:
uvx py-code-org who-uses implists the same packages as the site search.
With DuckDB you can run the whole check against the index on Hugging Face. This query lists the most downloaded projects that import any of the removed modules and have had no release since 13 November 2024, on the pinned 13 November 2026 snapshot. It counts every import, guarded or not, so check the file and line before you conclude that a package fails:
SELECT p.project,
p.downloads_30d,
CAST(p.upload_time AS DATE) AS last_release,
string_agg(DISTINCT r.module, ', ' ORDER BY r.module) AS removed_modules
FROM 'hf://datasets/pypi-data/py-code@9d81d70d6519d77f4dee4811b3e3a5d88379c17d/refs.parquet' r JOIN 'hf://datasets/pypi-data/py-code@9d81d70d6519d77f4dee4811b3e3a5d88379c17d/projects.parquet' p USING (project)
WHERE r.name = r.module
AND r.module IN ('distutils', 'imp', 'asyncore', 'asynchat', 'smtpd',
'aifc', 'audioop', 'cgi', 'cgitb', 'chunk', 'crypt', 'imghdr',
'lib2to3', 'mailcap', 'msilib', 'nis', 'nntplib', 'ossaudiodev',
'pipes', 'sndhdr', 'spwd', 'sunau', 'telnetlib', 'uu', 'xdrlib')
AND p.upload_time < '2024-11-13'
GROUP BY ALL
ORDER BY p.downloads_30d DESC
LIMIT 10;
It returned:
┌────────────────┬───────────────┬──────────────┬─────────────────────────────────────────────┐ │ project │ downloads_30d │ last_release │ removed_modules │ │ varchar │ int64 │ date │ varchar │ ├────────────────┼───────────────┼──────────────┼─────────────────────────────────────────────┤ │ future │ 36202649 │ 2024-02-21 │ distutils, imghdr, imp, lib2to3, sndhdr, uu │ │ humanfriendly │ 26687461 │ 2021-09-17 │ distutils, pipes │ │ coloredlogs │ 21574132 │ 2021-06-11 │ pipes │ │ ghp-import │ 13545359 │ 2022-05-02 │ pipes │ │ pydub │ 7478920 │ 2021-03-10 │ audioop │ │ avro-python3 │ 5785228 │ 2021-03-17 │ distutils │ │ crcmod │ 5688202 │ 2010-06-27 │ distutils │ │ docker-pycreds │ 5170286 │ 2018-11-29 │ distutils │ │ func-timeout │ 4256275 │ 2019-08-19 │ imp │ │ pycrypto │ 4211020 │ 2014-06-20 │ distutils │ └────────────────┴───────────────┴──────────────┴─────────────────────────────────────────────┘ 10 rows 4 columns
The scripts behind every number in this post, including the check that reads each import in context, are in the pypi-data/blog repository with their results.
How the numbers were counted
- Which projects. The index holds the latest release of 354,763 projects: every project released between 13 November 2025 and 13 November 2026, plus the 60,000 most downloaded projects even when their latest release is older. Projects with no release in the last year are only in it if they are among the most downloaded.
- Finding the imports. We took every project whose code imports one of the 25 modules by name (3,763 projects). For each one we downloaded the same release file again and parsed every
.pyfile with Python'sastmodule, without running it. - Guarded or not. An import counts as safe when it is inside a
tryblock that catchesImportError,ModuleNotFoundErrororException, inside anexceptblock (a fallback), or under anifthat tests the Python version. Everything else is unguarded. This is a rule, not a test: a guarded import whose fallback is also missing, as in pydub, is counted as safe. - Own code only. Imports in test files,
setup.py, docs and examples are not counted as unguarded. 1,548 projects import the modules only there, or their release is no longer on PyPI (3 projects). - Dependencies. 309 projects declare a copy of a module they import, such as
legacy-cgi,audioop-ltsor setuptools fordistutils, as a required dependency. Their imports of that module are not counted as unguarded. - Downloads and dates. Downloads are for the 30 days before the scan, from ClickHouse's public PyPI dataset, for the 60,000 most downloaded projects. "No release in two years" means the latest release was uploaded before 13 November 2024.
- Examples were read by hand. For every package named above as failing on import, we read the release's code to confirm that importing the package reaches the unguarded import. We did not install and run them.
Frequently asked questions
Why do I get ModuleNotFoundError: No module named 'distutils'?
Python 3.12 removed distutils from the standard library. The setuptools package still provides it, so pip install setuptools fixes the error. The lasting fix is to move the code to sysconfig, shutil.which or packaging. 2,565 indexed PyPI projects import distutils.
How do I fix No module named 'imp'?
The imp module was removed in Python 3.12. Replace it with importlib in your own code; for a dependency you cannot change, install zombie-imp, a copy of imp on PyPI, or replace the dependency, for example nose with pytest or boto with boto3.
Which modules were removed in Python 3.13?
aifc, audioop, cgi, cgitb, chunk, crypt, imghdr, mailcap, msilib, nis, nntplib, ossaudiodev, pipes, sndhdr, spwd, sunau, telnetlib, uu and xdrlib, the 19 dead batteries of PEP 594, plus lib2to3. Python 3.12 had already removed distutils, imp, asyncore, asynchat and smtpd.
How do I fix No module named 'cgi' on Python 3.13?
Replace cgi.escape with html.escape, cgi.parse_header with email.message, and cgi.FieldStorage with urllib.parse or the multipart library. For a dependency you cannot change, install legacy-cgi or standard-cgi, copies of the module on PyPI.
Why does pydub fail on Python 3.13?
pydub imports audioop, which Python 3.13 removed, and falls back to a pyaudioop module that does not exist. So import pydub fails with No module named 'pyaudioop'. Installing audioop-lts provides audioop again and fixes it.
How many PyPI packages still use removed standard library modules?
3,763 of the 354,763 projects in the py-code.org index import one of the 25 modules removed in Python 3.12 and 3.13. 1,623 of them have an import outside try/except or a version check in their own code. That import fails on Python 3.12 or 3.13 when its file or function is used.
Sources
- py-code.org dataset on Hugging Face, revision
9d81d70d(13 November 2026) - pypi-data/blog, the scripts and results for this post
- What's New in Python 3.12 and What's New in Python 3.13
- PEP 594: Removing dead batteries from the standard library and PEP 632: Deprecate distutils module
- Google Project Zero: From Naptime to Big Sleep (November 2024)
- DARPA: AI Cyber Challenge results (August 2025)
- Anthropic: 500+ high-severity vulnerabilities found by Claude Opus 4.6 (February 2026)
- Anthropic: disrupting the first reported AI-orchestrated cyber espionage campaign (November 2025)
- Mandiant M-Trends 2026 (March 2026)
- Daniel Stenberg: the end of the curl bug bounty (January 2026)
- Seth Larson: new era of slop security reports for open source (December 2024)
- PSF: ctx domain takeover (May 2022)
- OSV: PYSEC-2017-94 (pycrypto, CVE-2013-7459)
- Black Duck: 2026 Open Source Security and Risk Analysis (February 2026)
- pydub issue #839: No module named 'pyaudioop'
- ClickHouse PyPI downloads (ClickPy)