Python 类型注解实战:让 mypy 在上线前帮你抓 bug
很多人觉得 Python 类型注解就是「写着好看」「IDE 补全爽一点」,运行时又不检查,加不加无所谓。但真正的价值是:配合mypy做静态检查,能在代码跑起来之前抓到一大类低级错误——传错参数类型、函数忘了返回、None没处理。这篇用具体例子告诉你注解怎么写才有用,以及那些光看语法学不会的实战坑。
一、先看注解到底能拦住什么
假设有个函数,朴素写法不带注解:
defget_discount(price,level):iflevel=="vip":returnprice*0.8# 忘了写 else,非 vip 时隐式返回 Nonetotal=get_discount(100,"normal")+10# 运行时才炸:None + 10这个 bug 要等到线上跑到normal用户才暴露。加上注解后,mypy 直接在本地报错:
defget_discount(price:float,level:str)->float:iflevel=="vip":returnprice*0.8# mypy: Missing return statement —— 上线前就被拦下...运行mypy your_file.py,它会告诉你「声明返回 float,但有分支返回了 None」。这就是注解的核心价值:把一部分运行时错误提前到编码期。
二、Optional 不是可选参数,是「可能为 None」
新手最常见的误解:以为Optional[str]表示「这个参数可以不传」。错。Optional[X]就是X | None,表示「值可能是 X,也可能是 None」,和「参数有没有默认值」是两码事。
fromtypingimportOptional# 正确理解:返回值可能是 User,也可能是 None(没查到)deffind_user(uid:int)->Optional[User]:returndb.get(uid)# 查不到返回 Noneu=find_user(1)print(u.name)# mypy 报错:u 可能是 None,不能直接 .namemypy 会强制你先处理 None,这正是它值钱的地方——它逼你写出健壮代码:
u=find_user(1)ifuisnotNone:print(u.name)# 这个分支里 mypy 知道 u 一定是 User,放行Python 3.10+ 更推荐用X | None代替Optional[X],更直观:
deffind_user(uid:int)->User|None:# 等价,现代写法...三、容器类型:标注元素类型,别只写 list
list、dict光写外层等于没标。要写清楚里面装的是什么,mypy 才能帮你检查:
# 没用的标注:mypy 不知道元素类型defbad(items:list)->None:...# 有用的标注deftotal_prices(orders:list[dict[str,float]])->float:returnsum(o["amount"]foroinorders)# 传错结构会被抓到total_prices([{"amount":"99"}])# mypy 报错:value 应是 float,给了 strPython 3.9+ 直接用内置的list[...]、dict[...],不用再从 typing 导入List、Dict。
四、实战最好用:TypedDict 给「字典当对象用」上类型
Python 项目里到处是「用 dict 传结构化数据」,但 dict 的 key 拼错、类型错完全没提示。TypedDict能给这种字典加上精确的字段类型:
fromtypingimportTypedDictclassOrderDict(TypedDict):id:intamount:floatpaid:booldefprocess(order:OrderDict)->None:iforder["amount"]>100andorder["paid"]:print("大额已付款订单")# key 拼错、类型错都会被 mypy 抓出来process({"id":1,"amout":200.0,"paid":True})# ^^^^^ mypy 报错:多了 amout,少了 amount比起「注释里写一句 # order 是 {id, amount, paid}」,TypedDict 让检查工具真正读懂你的数据结构,重构时改字段名一改就全亮红。
五、别把 Any 当万金油
写不动类型就标Any,等于关掉 mypy 对这个变量的所有检查——它会「传染」:
fromtypingimportAnydefparse(data:Any)->Any:# 相当于放弃类型检查returndata["value"]# 后面全程无保护x=parse(something)# x 是 Any,之后怎么用都不报错,注解形同虚设对确实不定的结构,优先用object(强制你显式断言)或前面说的TypedDict/RawMessage思路,把Any的范围压到最小。滥用Any的项目,mypy 报告一片绿,实际一点没保护。
小结
- 类型注解的真正价值是配合
mypy在上线前抓错,不只是 IDE 补全。 Optional[X]==X | None,表示「可能是 None」,和参数是否可选无关;它逼你先判空。- 容器要标元素类型(
list[dict[str, float]]),只写list等于没标。 - 「字典当对象用」的场景上
TypedDict,key 拼错、类型错全被抓。 Any会关闭检查并传染,能不用就不用。
一句话记住:注解不是给人看的装饰,是给 mypy 下的断言;写得越具体,它替你抓的 bug 越多。