Python 从 3.5 起就支持类型注解(type hints)。这是这门动态语言现代化最重要的一步——让代码在保持灵活性的同时,获得静态类型检查的好处:IDE 精准补全、bug 早发现、文档即代码。这也是这个 50 篇教程的最后一站——学会类型注解,你就完成了从”能写 Python”到”写好 Python”的进化。
一、为什么要用类型注解
看这段没有注解的代码:
1 | def process(data, options): |
调用方一头雾水:data 是列表?字典?options 是什么形式?只能翻源码猜。
加上注解:
1 | def process(data: list[dict], options: ProcessOptions) -> Result: |
一目了然:data 是”字典的列表”,options 是某个类,返回 Result。IDE 也能自动补全 data[0]. 后的字段。
注解不改变运行时行为——Python 不会真的检查。它是给 IDE + mypy/pyright 看的。
二、基础语法
1 | name: str = "咖飞" |
- 变量:
名字: 类型 = 值(= 值可选); - 函数参数:
参数: 类型; - 返回值:
-> 类型; - 没有返回值用
-> None。
三、常用内置类型
1 | int # 整数 |
Python 3.9+ 直接用小写内置类型:list[int]。3.8 及以下要 from typing import List(List[int])。
四、Optional 与 Union
Optional[X] 表示”X 或 None”:
1 | def find_user(id: int) -> Optional[User]: |
Union[A, B] 表示”A 或 B”:
1 | def parse(x: Union[str, int]) -> str: |
五、Any:放弃类型检查
Any 表示”任意类型”——类型检查器会跳过它:
1 | from typing import Any |
用得越少越好——它是”我不知道类型”的最后手段。
六、Callable:函数类型
1 | from typing import Callable |
七、Literal:字面量类型
限定值只能是几个特定值:
1 | from typing import Literal |
八、TypedDict:字典的字段约束
普通字典 dict[str, Any] 太宽松。给字典字段加约束用 TypedDict:
1 | from typing import TypedDict |
九、协议 Protocol:结构化类型
前面 抽象类与接口 讲过。让你能表达”有某个方法就行”的类型:
1 | from typing import Protocol |
cleanup 接受任何有 close 方法的对象——不需要显式继承 SupportsClose。
十、泛型:TypeVar
想让函数”接受任意类型 T,返回同样的 T”:
1 | from typing import TypeVar |
约束泛型:
1 | from typing import TypeVar |
Python 3.12+ 新语法(更简洁):
1 | def first[T](items: list[T]) -> T: |
十一、泛型类
1 | from typing import Generic, TypeVar |
Python 3.12+:
1 | class Stack[T]: |
十二、dataclass 与类型注解
@dataclass 完美搭配注解——注解就是字段声明:
1 | from dataclasses import dataclass |
十三、装饰器与类型
装饰器保留类型信息需要一些技巧。简单的:
1 | from typing import Callable, TypeVar |
Python 3.10+ 的 ParamSpec 让参数类型精确传递:
1 | from typing import ParamSpec, TypeVar, Callable |
十四、mypy:静态类型检查器
安装:
1 | pip install mypy |
用:
1 | mypy my_script.py |
会报出所有类型不匹配。可以配置严格程度:
1 | # pyproject.toml |
类型注解只有配合 mypy(或 pyright/pylance)才有价值——单纯写注解不检查等于什么都没做。
十五、pyright / pylance
VS Code 的 Python 插件用 pylance(基于 pyright)实时检查类型——你写代码时红色波浪线就会立即出现。比 mypy 命令行更即时。
十六、TypeAlias 与 NewType
1 | from typing import TypeAlias, NewType |
Python 3.12+ 新语法:
1 | type Vector = list[float] |
十七、Self(Python 3.11+)
方法里返回 self 类型:
1 | from typing import Self |
十八、注解的运行时用途
虽然默认不检查,但可以用 typing.get_type_hints 反射:
1 | from typing import get_type_hints |
pydantic、dataclasses、FastAPI 都用这个来运行时校验数据。
十九、渐进式类型
Python 的类型注解是渐进式的——不用一次全部加上。策略:
- 先给公开 API 加(函数签名、类字段);
- 再给核心逻辑加;
- 最后处理边缘代码;
- 对无法确定的暂时用 Any。
二十、类型注解的成本 vs 收益
成本:多打字、初期学习曲线。
收益:
- IDE 补全大幅提升;
- 重构安全(改类型 mypy 立刻告诉你哪里坏);
- 参数类型清晰,代码即文档;
- 早发现 bug(None 传给不接受 None 的地方);
- 团队协作更顺畅。
规则:任何超过 100 行的项目,都值得加类型注解。
二十一、一个完整例子:类型化的小工具库
1 | # utils.py |
二十二、常见陷阱
- 注解字符串(Forward Reference):类内部引用自己时要引号或
from __future__ import annotations。 - 循环导入:注解触发的 import 陷入循环。放
if TYPE_CHECKING:里。 - runtime 不检查:mypy 报错但代码还是能跑。别以为报了错就不能运行。
Any传染:一处 Any 会让检查失效一大片。- 过度用泛型:小项目用不到。用得越多,读者理解成本越高。
isinstance(x, list[int]):不能这么写——运行时list[int]不能用于 isinstance。用isinstance(x, list)。- 注解与运行时行为不一致:
def f(x: int) -> None:传字符串照跑不误——类型只是”建议”。
二十三、小结与延伸阅读
- 类型注解是”运行时不检查、给工具看”的元数据;
- 基础:
int、str、list[X]、dict[K, V]、T | None; - 泛型:
TypeVar、Generic、3.12+[T]语法; - 复杂类型:
Callable、Protocol、Literal、TypedDict、NewType; - 用
@dataclass时字段声明就是注解; - 装 mypy / 用 pylance 做静态检查;
- 渐进式加,先公开 API,后内部;
- 团队项目强烈推荐。
延伸阅读:
- typing:https://docs.python.org/zh-cn/3/library/typing.html
- mypy:https://mypy.readthedocs.io/
- Python 类型系统 PEP 全景:https://peps.python.org/topic/typing/
🎉 50 篇完结感言
从 Python 简介 开始,历经环境搭建、基础语法、数据结构、函数与模块、面向对象、异常与文件、迭代与生成器、装饰器、标准库、日志、HTTP、并发、异步、到今天的类型注解——50 篇文章正式结束。
如果你从头到尾跟着敲了一遍代码:
- 你已经掌握了 Python 从入门到中级的完整技能栈;
- 你能读懂大部分开源项目的代码;
- 你有能力独立完成 Web 服务、爬虫、数据处理、自动化脚本;
- 你理解了 GIL、协程、装饰器背后的原理。
接下来的路:
- 深入某个方向:Web(FastAPI/Django)、数据(Pandas/NumPy)、AI(PyTorch/LangChain)、爬虫(Scrapy/Playwright);
- 读源码:Flask、requests、Django 都是很好的阅读对象;
- 写项目:任何”你想做的”东西——博客、量化、机器人、聊天室;
- 参与开源:GitHub 上找 good-first-issue 试试贡献。
“人生苦短,我用 Python”。愿你在 Python 世界里享受写代码的快乐。
—— 咖飞,2025 年 9 月 30 日