PyPI code that breaks on Python 3.12 and 3.13

Published 14 November 2026 · Data as of 13 November 2026

Scripts and results for this post on GitHub

PyPI code that breaks on Python 3.12 and 3.13Data as of 13 Nov 2026Projects that importa removed module3,763of 354,763 indexed projects1,623with an unguarded import intheir own codeOf the 60,000 mostdownloaded852with an unguarded import544of them with no release in twoyearsDownloads of thosestale projects126Min the 30 days before the scanimport nose and import boto fail on Python 3.12 and later, yet the two still get 5.2Mdownloads a month. Their last releases: 2015 and 2018.Removed in 3.12: distutils, imp, asyncore, asynchat, smtpd. In 3.13: the 19 PEP 594modules and lib2to3. Latest release of 354,763 projects. py-code.org
PyPI code that breaks onPython 3.12 and 3.13Data as of 13 Nov 2026Projects that import a removed module3,763of 354,763 indexedprojects1,623with an unguardedimport in theirown codeOf the 60,000 most downloaded852with an unguardedimport544of them with norelease in twoyearsDownloads of those stale projects126Min the 30 days before the scanimport nose and import boto fail onPython 3.12 and later, yet the twostill get 5.2M downloads a month. Theirlast releases: 2015 and 2018.Removed in 3.12: distutils, imp,asyncore, asynchat, smtpd. In 3.13: the19 PEP 594 modules and lib2to3. Latestrelease of 354,763 projects. py-code.org

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

QuestionAnswerKey number
How many PyPI projects import a removed module?1.1% of the indexed projects3,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 code1,623 projects
How many of the 60,000 most downloaded projects have one?852 have an unguarded import544 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.131.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.

ModuleRemoved inProjects that import itUnguardedReplacement
distutils3.122,565973setuptools (still ships distutils), or sysconfig, shutil.which, packaging
imp3.12614246importlib; zombie-imp on PyPI
cgi3.13455248urllib.parse, email.message; legacy-cgi on PyPI
audioop3.1315152audioop-lts on PyPI
imghdr3.1311485filetype, puremagic; standard-imghdr on PyPI
pipes3.1310737subprocess, shlex.quote
telnetlib3.138560telnetlib3, Exscript; standard-telnetlib on PyPI
crypt3.136718hashlib, bcrypt, argon2-cffi; crypt_r on PyPI
asyncore3.124628asyncio; pyasyncore on PyPI
lib2to33.133527none in the standard library
cgitb3.132520legacy-cgi on PyPI
xdrlib3.132114standard-xdrlib on PyPI
aifc3.131511standard-aifc on PyPI
chunk3.131511standard-chunk on PyPI
uu3.131412base64
smtpd3.12107aiosmtpd on PyPI
sndhdr3.13109filetype, puremagic; standard-sndhdr on PyPI
msilib3.13108none
asynchat3.1294asyncio; pyasynchat on PyPI
nntplib3.1394pynntp; standard-nntplib on PyPI
spwd3.1370python-pam
mailcap3.1361mimetypes
ossaudiodev3.1330pygame
sunau3.1321standard-sunau on PyPI

No indexed project imports nis.

Projects with an unguarded import of eachremoved moduleprojectsdistutils973cgi248imp246imghdr85telnetlib60audioop52pipes37asyncore28lib2to327cgitb20
Projects with at least one unguarded import of each module in their own code, for the ten modules with the most. An unguarded import is not inside 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 dockerpycreds loads distutils.spawn
  • aioredis (1.3M, last release 2021): aioredis.connection imports distutils.version
  • arsenic (1.2M, last release 2021), python-editor (0.7M, 2019) and GPUtil (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 nose loads nose.importer, which starts with import imp.
  • boto, the AWS SDK that boto3 replaced, gets 3.4M downloads a month with a last release in 2018. import boto loads boto.plugin, which imports imp.

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) and htmlmin (0.7M, 2017) import cgi. coreapi uses it for cgi.parse_header, and htmlmin's main.py imports it without using it.
  • pgpy (0.8M, 2022) imports imghdr in pgpy.constants, which import pgpy loads.
  • coloredlogs (21.6M, 2021) and humanfriendly (26.7M, 2021) import pipes at the top of the modules behind their command-line tools. The libraries still import, but the coloredlogs and humanfriendly commands 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.

ProjectDownloads, 30 daysLast releaseUnguarded imports ofFirst unguarded import
future36.2M2024imghdr, lib2to3, sndhdr, uufuture/backports/email/message.py:13
humanfriendly26.7M2021pipeshumanfriendly/cli.py:82
coloredlogs21.6M2021pipescoloredlogs/converter/__init__.py:12
docker-pycreds5.2M2018distutilsdockerpycreds/utils.py:1
pytzdata4.1M2020distutilspytzdata/commands/make.py:11
boto3.4M2018cgi, impboto/plugin.py:40
oauth2client2.5M2018cgioauth2client/contrib/appengine.py:20
Pympler2.5M2024cgipympler/util/bottle.py:38
nose1.8M2015impnose/importer.py:12
memory-profiler1.6M2022distutilsmemory_profiler.py:941
Most downloaded projects with no releasesince November 2024 and an unguardedremoved importmillion downloads in 30 days (year of last release)future (2024)36.2humanfriendly (2021)26.7coloredlogs (2021)21.6docker-pycreds (2018)5.2pytzdata (2020)4.1boto (2018)3.4oauth2client (2018)2.5Pympler (2024)2.5nose (2015)1.8memory-profiler (2022)1.6
The ten most downloaded projects that have an unguarded import of a removed module and no release since 13 November 2024, with their downloads in the 30 days before the scan. The year in brackets is the year of the last release.

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 ctx maintainer, 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 the mailcap module "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 imp lists 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 .py file with Python's ast module, without running it.
  • Guarded or not. An import counts as safe when it is inside a try block that catches ImportError, ModuleNotFoundError or Exception, inside an except block (a fallback), or under an if that 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-lts or setuptools for distutils, 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