容易中招的多半是那种放在共享盘里好几年没人改过的小脚本:读一份当年用记事本存成 ANSI 的配置,拼几行字,再导出一份给别的老程序吃的文本。在中文 Windows 上,open() 不写 encoding 时一直按系统区域的编码来,也就是 GBK 那一路,所以一直没出过事。换上 Python 3.15 以后,这些脚本要么一开头就抛 UnicodeDecodeError,要么跑完了,下游打开一看全是乱码。
改了什么
python.org 上 3.15.0 的发布页写着 2026 年 10 月 9 日发布,页面顶上已经提示被 3.15.2 取代。3.15.2 是一个加急的安全更新,10 月 25 日发布。下面说的默认编码变化,是整个 3.15 系列的行为。
What’s New in Python 3.15 里的说法是:Python 现在默认用 UTF-8,不再看系统环境,像 open('xxx.txt') 这样不带 encoding 的读写都按 UTF-8 走。PEP 686 说得更全,文件、标准输入输出和管道都一样。PEP 里也承认,Unix 上大多本来就是 UTF-8,这次主要影响的是 Windows。依赖默认编码的程序会遇到 UnicodeError、乱码,甚至数据被悄悄写坏。
最后这种很难察觉。读 GBK 文件报错,起码当场就知道。脚本写出去的文件从 GBK 变成了 UTF-8,自己读回来没问题,可换成只认本地编码的老程序去读就露馅了,那时离升级可能已经过去好几天。
找出没写 encoding 的地方
io 模块的文档里有个可选的编码警告,3.10 起就有:运行时加 -X warn_default_encoding,或者设环境变量 PYTHONWARNDEFAULTENCODING,凡是用到默认编码的地方都会发一条 EncodingWarning。把测试或者常用的几个入口这样跑一遍,警告指向的行就是要改的地方。
除了 open(),下面几处也容易漏掉:
- 用
subprocess加text=True读命令输出时,如果调用的是输出本地编码的 Windows 命令,现在会按 UTF-8 去解,结果就是乱码。PEP 686 给的改法是把text=True换成显式的encoding="utf-8"或encoding="locale"。 - 有的代码用
locale.getpreferredencoding()拿编码再往下传,可 UTF-8 模式会影响这个函数的返回值。PEP 建议改成直接写"utf-8",或者用 3.11 加进来的locale.getencoding()。 - 还有
pathlib的read_text(),文档写明这个方法的参数含义和open()一样,不传 encoding 就是同一套默认编码。 - 标准流也跟着变。os 模块文档写着,UTF-8 模式下
sys.stdin、sys.stdout、sys.stderr都按 UTF-8 编码。脚本输出要是被重定向到文件、再交给批处理或别的程序读,print()写出去的内容编码也就变了。标准流可以单独用PYTHONIOENCODING覆盖。
另外,UTF-8 模式下 open() 默认仍是 strict 错误处理,遇到解不开的字节会直接抛异常,不会悄悄塞进替换字符。所以读的那一侧多半是当场报错,写的那一侧更容易在没人察觉时出事。
怎么补
encoding="locale" 是给「确实要按系统编码读写」的场景准备的,3.10 就支持了。PEP 686 提到,这个写法在 UTF-8 模式下的行为从 3.11 起已经修正,会照常按本地编码处理。跟老系统交换文件、对方改不了的那一侧,用这个写法正合适。
实在来不及改代码,可以先把 UTF-8 模式关掉,回到旧行为:设环境变量 PYTHONUTF8=0,或者命令行加 -X utf8=0。这只适合救急,比如定时任务马上要跑、代码一时改不完。
长期还是得做两件事:手里的文本文件统一转成 UTF-8,所有读写的地方都把 encoding 写明白。What’s New 里也这么建议:想在多个 Python 版本之间都保持一致,就始终显式传 encoding。
还没升级的项目也能提前摸底:UTF-8 模式从 3.7 就有了(由 PEP 540 引入,os 模块文档的 Python UTF-8 Mode 一节有说明),在旧版本上把这个模式打开跑一遍测试,3.15 会出的问题大多能提前冒出来。
各位的老脚本里,没写 encoding 的地方大概有多少处?