Python 3.9 于 2020 年 10 月发布,是 2.x 彻底退出历史舞台之后、3.x 快速迭代期里一个容易被低估的版本。它没有 3.8 海象运算符那样抓眼球,却悄悄补齐了一批"写业务代码天天用得上"的能力:合并字典终于有了专属运算符 |,类型提示终于可以直接写 list[str] 而不是 List[str],字符串去前缀后缀有了专门方法,时区数据库也进了标准库。本文带你逐一上手这些特性,并给出务实的升级建议。

1. 字典合并运算符 | 与 |=

在 3.9 之前,合并两个字典最常用的写法是 {**a, **b} 或者 a.update(b)。前者能生成新字典但可读性一般,后者是就地修改且返回 None,想"合并出一个新字典"总得绕弯子。PEP 584 为字典引入了 ||= 两个运算符,让合并表达得干净利落:

a = {"x": 1, "y": 2}
b = {"y": 3, "z": 4}

merged = a | b        # 返回新字典,不修改 a、b
print(merged)         # {'x': 1, 'y': 3, 'z': 4}

a |= b                # 就地合并,等价于 a.update(b)
print(a)              # {'x': 1, 'y': 3, 'z': 4}

键冲突时的规则与 update 完全一致:右边的值胜出。区别在于语义:| 生成全新对象,适合"合并配置、不污染原数据"的函数式写法;|= 就地修改,适合逐层叠加默认值。一个典型场景是配置合并——先定义默认配置,再用用户配置覆盖:

defaults = {"host": "localhost", "port": 3306, "debug": False}
user_cfg = {"port": 5432}

final = defaults | user_cfg
print(final)  # {'host': 'localhost', 'port': 5432, 'debug': False}

注意:合并运算符在 3.9 中只支持 dict 本身,3.10 之后才放宽到其它映射类型。

还有一个容易忽略的点:| 是"浅合并"——如果两个字典的某个键都对应嵌套字典,结果会整体替换而不是递归合并。需要深合并时,仍要自己写递归或借助第三方库,这一点与 update 的行为一致。

写法行为返回值
{**a, **b}合并出新字典(3.9 前常用)新字典
a | b合并出新字典(3.9 起)新字典
a |= b就地合并,等价于 a.update(b)None

2. 类型提示泛型:list[str] 时代到来

PEP 585 让内置容器类型可以直接携带类型参数:list[str] 就是"字符串列表",dict[str, int] 就是"键为字符串、值为整数的字典"。此前必须从 typing 导入大写的 ListDict,写法啰嗦,而且运行时还要多做一次类型检查。3.9 起这些大写别名被标记为废弃,官方建议一律改用小写内建类型,IDE 与类型检查器(mypy、pyright)的体验也随之更好:

def first_word(words: list[str]) -> str:
    return words[0] if words else ""

scores: dict[str, int] = {"math": 90, "english": 85}

from typing import Optional

def find_user(uid: int) -> Optional[dict]:
    """注意:dict | None 联合类型要等 3.10,3.9 请用 Optional"""
    ...

一个容易踩的坑:联合类型运算符 | 在类型标注里是 3.10 才支持的(PEP 604),3.9 中表达"可能是 A 也可能是 B"仍要写 Optional[...]Union[...]。3.9 的泛型只覆盖"容器元素类型"这一层。

配合类型检查器使用,收益更大:mypy 能根据 list[str] 自动推断 words[0] 的类型是 str,拼错字符串方法名时 IDE 会第一时间划红线。从"运行时防御"转向"静态检查",是 3.9 之后写 Python 的主流姿势,值得尽早养成。

3. 字符串新方法:removeprefix 与 removesuffix

处理文件名、URL、日志前缀时,我们经常要"去掉开头/结尾的固定字符串"。以前得先 startswith 判断再切片,或者动用正则;PEP 616 增加了两个直白的方法,而且前缀不匹配时原样返回,不会误伤:

name = "HelloWorld.txt"
print(name.removeprefix("Hello"))   # World.txt
print(name.removesuffix(".txt"))    # HelloWorld
print(name)                         # HelloWorld.txt,原字符串不变

一个典型场景是批量重命名文件:遍历目录,对每个文件名去掉 .tmp 后缀再改名,逻辑从"切片加判断"变成一行方法调用,代码意图一目了然。相比 lstrip,removeprefix 按完整前缀匹配,不会误删字符串中间的字符。

4. zoneinfo:内置时区数据库

处理跨时区时间以前几乎必须装第三方库 pytz。3.9 的 zoneinfo 模块把 IANA 时区数据库直接内置,配合标准库 datetime 就能拿到带时区的时间:

from datetime import datetime
from zoneinfo import ZoneInfo

now = datetime.now(ZoneInfo("Asia/Shanghai"))
print(now)  # 2026-08-05 13:06:00+08:00

tokyo = now.astimezone(ZoneInfo("Asia/Tokyo"))
print(tokyo)  # 自动换算时区

注意 datetime.now() 默认返回本地时区的 naive 时间,不带时区信息;用 ZoneInfo 构造的时间是 aware 的,可以直接比较大小、做加减。跨时区调度、定时任务里用它,不会再出现"差八小时"的经典 bug。

5. 其它 3.9 更新速览

6. 如何安装 Python 3.9

推荐用 pyenv 管理多版本,一条命令装好并切换:

# 安装并启用 3.9
pyenv install 3.9.21
pyenv local 3.9.21

# 验证并准备独立环境
python3.9 --version
python3.9 -m pip install --upgrade pip
python3.9 -m venv .venv
source .venv/bin/activate

7. 值不值得升级?

说句实在话:到 2026 年,Python 3.9 已经停止官方维护(安全更新于 2025 年 10 月结束),新项目千万不要选它。它的真正价值在于"历史坐标":如果你还在维护跑在 3.6/3.7 上的老代码,把版本升到 3.9 是性价比很高的一步——升级成本低、第三方库兼容性好,还能立刻用上字典合并、内建泛型、removeprefix 等特性,为之后跳到 3.11+ 铺平道路。换个角度看,3.9 引入的 PEP 617(PEG 解析器)虽然看不见摸不着,却让后续所有新语法(match、类型参数)的落地成为可能。一句话总结:3.9 是过渡版本,学它理解演进脉络,但别让它当生产主力。

实际操作上,从 3.6/3.7 升到 3.9,最常见的兼容性问题是 async/await 保留字冲突与旧式 f-string,但绝大多数第三方库早已适配;先跑一遍测试、再灰度上线,一天内就能完成。而如果你已经在 3.8 上,直接跳到 3.11 会更划算——3.9 的维护期已经结束,没必要再停一站。

💡 学习建议:把配置合并的代码改成 defaults | user_cfg,再全局搜索项目里的 List[Dict[ 替换成小写内建泛型,你会直观感受到"新语法让代码更短、更接近人话"。