page contents

几个被淘汰的 Python 库,请不要再用!

Python 3.12 移除了 distutils、imp、asyncore、asynchat、smtpd 等模块;到了 Python 3.13,又一次性移除了 19 个 PEP 594 里列出的老模块,包括 cgi、imghdr、telnetlib、pipes、crypt 等。

attachments-2026-10-Fo6zImfo6ac5a5f471899.png前两天翻一个老 Python 项目,换了下基础镜像,代码还没跑到业务逻辑就死了。

ModuleNotFoundError: No module named 'distutils'

另一个脚本更直接:

ModuleNotFoundError: No module named 'cgi'

这种错误我一般不急着 pip install。

先看 Python 版本。

因为有些库压根不是“你没装”,而是 Python 官方已经把它从标准库里清出去了。

Python 3.12 移除了 distutils、imp、asyncore、asynchat、smtpd 等模块;到了 Python 3.13,又一次性移除了 19 个 PEP 594 里列出的老模块,包括 cgi、imghdr、telnetlib、pipes、crypt 等。

所以项目里要是还躺着下面这些代码,我建议别等升级 Python 的时候再处理。

distutils,别再拿它做版本判断和打包了

以前很多脚本喜欢这么写:

from distutils.version import LooseVersion

if LooseVersion(remote_ver) > LooseVersion(local_ver):
    upgrade()

这代码在老项目里相当常见。

问题是 distutils 从 Python 3.10 开始弃用,到 Python 3.12 已经正式从标准库移除。

版本比较我现在直接用 packaging。

比如一个部署脚本,要判断服务端插件是不是比本地新:

from packaging.version import Version

def need_pull(current: str, published: str) -> bool:
    return Version(published) > Version(current)

if need_pull("2.9.8", "2.10.1"):
    print("发现新版本,准备拉取")

这地方千万别自己:

"2.10.1" > "2.9.8"

字符串比较版本号,迟早给自己挖坑。

还有一种情况比较迷惑。

你升级 Python 3.12 后,某些环境里 import distutils 居然还能成功,于是觉得官方不是说移除了吗?

很可能是 setuptools 给你兜了一层兼容。

Python 官方文档也明确提到,第三方的 Setuptools 仍然可以提供 distutils,但 Python 3.12 创建虚拟环境时连 setuptools 都不再默认预装。

所以别拿“我电脑还能跑”当依据。

CI 换台机器可能就炸了。

imp,动态加载代码的老项目里特别多

我见过不少插件系统、规则脚本、测试工具还留着这种东西:

import imp

module = imp.load_source("checker", "/opt/rules/checker.py")

这个模块其实早就该清了。

imp 从 Python 3.4 就开始弃用,一直拖到 Python 3.12 才彻底移除,官方给的迁移方向就是 importlib。

现在动态加载本地规则文件,我一般这么写:

import importlib.util
from pathlib import Path


def load_rule(rule_file: Path):
    module_name = f"_rule_{rule_file.stem}"

    spec = importlib.util.spec_from_file_location(module_name, rule_file)

    if spec is None or spec.loader is None:
        raise RuntimeError(f"规则文件无法加载: {rule_file}")

    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)

    return module


rule = load_rule(Path("/srv/rules/check_amount.py"))
result = rule.check({"amount": 3800})

代码是多了几行。

但动态加载这种事情,我反而不喜欢一句话全藏掉。

模块从哪里来的、loader 有没有拿到、哪一步加载失败,都应该看得见。

出问题的时候好查。

asyncore、asynchat,老 TCP 服务别再往上续命

这个我印象比较深。

维护过一些年份比较久的 TCP 服务,尤其设备通信、内部代理、Socket 服务,很容易翻出:

import asyncore

你看到它的时候基本就该提高警惕了。

asyncore 和 asynchat 从 Python 3.6 就已经弃用,到 Python 3.12 正式删除,官方给出的替代就是 asyncio。

比如我现在写一个很轻量的设备探测服务,不会再围着 asyncore.dispatcher 搭东西:

import asyncio

async def receive_probe(reader, writer):
    peer = writer.get_extra_info("peername")

    try:
        payload = await asyncio.wait_for(
            reader.readline(),
            timeout=5
        )

        device_code = payload.decode(errors="replace").strip()

        print(f"probe from={peer}, device={device_code}")

        writer.write(b"OK\n")
        await writer.drain()

    except TimeoutError:
        print(f"probe timeout, peer={peer}")

    finally:
        writer.close()
        await writer.wait_closed()


async def serve():
    server = await asyncio.start_server(
        receive_probe,
        host="0.0.0.0",
        port=9108
    )

    async with server:
        await server.serve_forever()


asyncio.run(serve())

不是说换成 asyncio 就天然高性能。

线程、事件循环、阻塞调用这些坑一个都不会自动消失。

但你现在还继续给 asyncore 写新代码,就有点跟 Python 版本升级硬扛了。

没必要。

smtpd 被删了,但别把 smtplib 一起误杀

这两个名字很容易看串。

smtpd 是拿来做 SMTP Server 的,Python 3.12 已经移除了,官方推荐考虑基于 asyncio 的第三方库 aiosmtpd。

但是:

import smtplib

这个不是一回事。

你业务代码只是连接公司的 SMTP 服务发邮件,smtplib 还在。

所以代码扫描的时候别看到 SMTP 三个字母就开始全局替换。

这种升级我最烦“一把梭”。

先看模块到底拿来干什么。

cgi,看到这个名字基本已经有年代感了

Python 3.13 项目里再执行:

import cgi

已经没了。

cgi 从 Python 3.11 弃用,Python 3.13 正式移除,最后一个自带它的版本是 Python 3.12。

老代码里比较典型的是:

cgi.FieldStorage()

用来自己处理表单和上传参数。

这种代码新项目我一般不会迁成另一套“手撸 HTTP 表单解析”。

如果已经用了 FastAPI、Django、Flask 之类的框架,就让框架处理。

如果只是一个很薄的 WSGI 工具,需要读 Query String,标准库已经够了:

from urllib.parse import parse_qs


def read_filters(environ):
    raw_query = environ.get("QUERY_STRING", "")

    params = parse_qs(
        raw_query,
        keep_blank_values=True
    )

    return {
        "keyword": params.get("keyword", [""])[0],
        "page": int(params.get("page", ["1"])[0])
    }

至于 multipart/form-data 文件上传,别为了少装一个依赖,自己在那里切 boundary。

我见过这种代码。

刚开始几十行,后面修中文文件名、重复字段、大文件、异常报文,越修越不像人写的。

Python 官方对 cgi.FieldStorage 的迁移建议里,也给出了 urllib.parse、email.message 以及 multipart 第三方库这些方向。

PyPI 上虽然还有 legacy-cgi 可以给老系统续一下命,但如果是新项目,我不会重新把它接回来。

imghdr,上传图片的代码里经常还能翻出来

这个更容易中招:

import imghdr

image_type = imghdr.what(file_path)

很多头像上传、图片导入、批处理脚本以前都这么判断图片类型。

Python 3.13 已经把 imghdr 移除了。

官方现在给出的替代包括 filetype、puremagic、python-magic。

比如上传目录里做第一层文件检查,可以这么搞:

from pathlib import Path
import filetype


ALLOW_MIME = {
    "image/jpeg",
    "image/png",
    "image/webp",
}


def check_upload(path: Path):
    kind = filetype.guess(path)

    if kind is None:
        raise ValueError("无法识别文件格式")

    if kind.mime not in ALLOW_MIME:
        raise ValueError(f"不允许上传该类型: {kind.mime}")

    return kind.mime

不过这里我还得多说一句。

识别出来是 image/jpeg,不代表这个文件就绝对安全。

真是用户上传场景,我通常还会让图片库实际解码一次,再做尺寸、像素、文件大小限制。

只看:

xxx.jpg

这个后缀是最不值得信的东西。

telnetlib,设备项目和运维脚本重点搜一下

一般 Web 项目不太能碰见它。

做过交换机、路由器、工控设备、老服务器自动化的,代码仓库里没准现在还有:

import telnetlib

Python 3.13 已经移除了 telnetlib。官方列出的可选替代包括 telnetlib3 和 Exscript。

这种代码我不会直接机械替换。

我一般先问一句:

这台设备现在还必须用 Telnet 吗?

如果设备已经支持 SSH,那我宁可顺手把协议也换掉。

如果是二十年前的设备,固件动不了,现场就认 Telnet,那再考虑 telnetlib3。

技术债有时候不是换个 import 就算还完。

pipes,拼 Shell 命令的代码该收拾了

老脚本里还可能见到:

import pipes

尤其是拼 shell 参数。

Python 3.13 已经把它移除,官方迁移建议很直接:

跑子进程,用 subprocess

只是需要 shell 参数转义,用 shlex.quote()

但我写自动化脚本时,更喜欢从源头上少拼 shell 字符串。

比如备份一个目录:

import subprocess
from pathlib import Path

def archive_dir(source: Path, output: Path):
    cmd = [
        "tar",
        "-czf",
        str(output),
        "-C",
        str(source.parent),
        source.name,
    ]

    result = subprocess.run(
        cmd,
        check=True,
        capture_output=True,
        text=True,
    )

    return result.stderr

这里故意没有:

shell=True

路径也不用自己在那里加引号、转义空格、判断特殊字符。

能传参数列表的时候,就别先拼成一坨字符串再交给 Shell 重新解析。

省很多烂问题。

还有一个我建议认证系统重点搜:crypt

Python 3.13 同样把 crypt 模块移除了。对于密码哈希,官方文档给出的替代方向包括 bcrypt、argon2-cffi 等。

新代码如果还在自己折腾:

crypt.crypt(password, salt)

我一般直接改。

比如使用 Argon2:

from argon2 import PasswordHasher
password_hasher = PasswordHasher()
def encode_password(raw_password: str) -> str:
    return password_hasher.hash(raw_password)
def verify_password(encoded: str, raw_password: str) -> bool:
    try:
        return password_hasher.verify(encoded, raw_password)
    except Exception:
        return False

认证这一块尤其别有“原来能跑就不动”的想法。

能跑和该不该继续跑,是两回事。

如果手里有个几年没升级过 Python 的项目,我不会一个目录一个目录肉眼翻。

先扫一遍:

rg -n '\b(import|from)\s+(distutils|imp|asyncore|asynchat|smtpd|cgi|imghdr|telnetlib|pipes|crypt)\b' .

再把测试环境拉到准备升级的 Python 版本:

python -Wd -m pytest

-Wd 我挺喜欢加。

有些弃用警告平时被藏得太好了,开发几年都没人看,直到某个大版本直接把 API 删除,CI 才突然一片红。

Python 这几年清理标准库的动作并不小。

特别是 3.12 和 3.13。后者按照 PEP 594 一口气清掉了 19 个被认为过时、历史遗留或存在更好替代方案的标准库模块。

所以升级 Python,我现在第一件事一般不是研究新语法。

先搜 import。

老代码里那几个看着特别眼熟、十几年没动过的库,往往比业务代码先给你一拳。

更多相关技术内容咨询欢迎前往并持续关注好学星城论坛了解详情。

想高效系统的学习Python编程语言,推荐大家关注一个微信公众号:Python编程学习圈。每天分享行业资讯、技术干货供大家阅读,关注即可免费领取整套Python入门到进阶的学习资料以及教程,感兴趣的小伙伴赶紧行动起来吧。

attachments-2022-05-rLS4AIF8628ee5f3b7e12.jpg

 

你可能感兴趣的文章

相关问题

0 条评论

请先 登录 后评论
Pack
Pack

2480 篇文章

作家榜 »

  1. Pack 2480 文章
  2. 轩辕小不懂 2403 文章
  3. 小柒 2228 文章
  4. Nen 576 文章
  5. 王昭君 216 文章
  6. 文双 71 文章
  7. 小威 64 文章
  8. Cara 36 文章