前两天翻到一个老 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
defneed_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
defload_rule(rule_file: Path):
module_name = f"_rule_{rule_file.stem}"
spec = importlib.util.spec_from_file_location(module_name, rule_file)
if spec isNoneor spec.loader isNone:
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
asyncdefreceive_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()
asyncdefserve():
server = await asyncio.start_server(
receive_probe,
host="0.0.0.0",
port=9108
)
asyncwith 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
defread_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",
}
defcheck_upload(path: Path):
kind = filetype.guess(path)
if kind isNone:
raise ValueError("无法识别文件格式")
if kind.mime notin 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
defarchive_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()
defencode_password(raw_password: str) -> str:
return password_hasher.hash(raw_password)
defverify_password(encoded: str, raw_password: str) -> bool:
try:
return password_hasher.verify(encoded, raw_password)
except Exception:
returnFalse
认证这一块尤其别有“原来能跑就不动”的想法。
能跑和该不该继续跑,是两回事。
如果手上有个几年没升级过 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。
老代码里那几个看着特别眼熟、十几年没动过的库,往往比业务代码先给你一拳。