类型注解 type hints 实战

Python 从 3.5 起就支持类型注解(type hints)。这是这门动态语言现代化最重要的一步——让代码在保持灵活性的同时,获得静态类型检查的好处:IDE 精准补全、bug 早发现、文档即代码。这也是这个 50 篇教程的最后一站——学会类型注解,你就完成了从”能写 Python”到”写好 Python”的进化。

一、为什么要用类型注解

看这段没有注解的代码:

1
2
def process(data, options):
...

调用方一头雾水:data 是列表?字典?options 是什么形式?只能翻源码猜。

加上注解:

1
2
def process(data: list[dict], options: ProcessOptions) -> Result:
...

一目了然:data 是”字典的列表”,options 是某个类,返回 Result。IDE 也能自动补全 data[0]. 后的字段。

注解不改变运行时行为——Python 不会真的检查。它是给 IDE + mypy/pyright 看的。

二、基础语法

1
2
3
4
5
6
7
8
9
10
name: str = "咖飞"
age: int = 3
prices: list[float] = [3.14, 2.71]
data: dict[str, int] = {"a": 1, "b": 2}

def add(a: int, b: int) -> int:
return a + b

def greet(name: str) -> None:
print(f"hi {name}")
  • 变量:名字: 类型 = 值= 值 可选);
  • 函数参数:参数: 类型
  • 返回值:-> 类型
  • 没有返回值用 -> None

三、常用内置类型

1
2
3
4
5
6
7
8
9
10
11
12
int          # 整数
float # 浮点
str # 字符串
bool # 布尔
bytes # 字节
None # 空值
list[X] # 元素为 X 的列表
tuple[X, Y] # 定长元组
tuple[X, ...] # 可变长同类型元组
dict[K, V] # 字典
set[X] # 集合
frozenset[X]

Python 3.9+ 直接用小写内置类型:list[int]。3.8 及以下要 from typing import ListList[int])。

四、Optional 与 Union

Optional[X] 表示”X 或 None”:

1
2
3
4
5
6
def find_user(id: int) -> Optional[User]:
"""找不到时返回 None。"""

# Python 3.10+ 更简洁:
def find_user(id: int) -> User | None:
...

Union[A, B] 表示”A 或 B”:

1
2
3
4
5
6
def parse(x: Union[str, int]) -> str:
return str(x)

# Python 3.10+
def parse(x: str | int) -> str:
...

五、Any:放弃类型检查

Any 表示”任意类型”——类型检查器会跳过它

1
2
3
4
from typing import Any

def call_anything(obj: Any) -> Any:
return obj.something() # 类型检查器不管

用得越少越好——它是”我不知道类型”的最后手段。

六、Callable:函数类型

1
2
3
4
5
6
7
8
from typing import Callable

def apply(func: Callable[[int, int], int], a: int, b: int) -> int:
return func(a, b)

# 参数任意
def apply_any(func: Callable[..., int]) -> int:
return func()

七、Literal:字面量类型

限定值只能是几个特定值:

1
2
3
4
5
6
7
from typing import Literal

def align(text: str, side: Literal["left", "right", "center"]) -> str:
...

align("hi", "left") # ✅
align("hi", "top") # ⚠️ 类型检查报错

八、TypedDict:字典的字段约束

普通字典 dict[str, Any] 太宽松。给字典字段加约束用 TypedDict

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from typing import TypedDict

class UserDict(TypedDict):
name: str
age: int
email: str


def process(u: UserDict) -> None:
print(u["name"], u["age"])


process({"name": "咖飞", "age": 3, "email": "x@y.com"}) # ✅
process({"name": "咖飞", "age": 3}) # ⚠️ 缺 email

九、协议 Protocol:结构化类型

前面 抽象类与接口 讲过。让你能表达”有某个方法就行”的类型:

1
2
3
4
5
6
7
8
from typing import Protocol

class SupportsClose(Protocol):
def close(self) -> None: ...


def cleanup(x: SupportsClose) -> None:
x.close()

cleanup 接受任何有 close 方法的对象——不需要显式继承 SupportsClose。

十、泛型:TypeVar

想让函数”接受任意类型 T,返回同样的 T”:

1
2
3
4
5
6
7
8
9
10
from typing import TypeVar

T = TypeVar("T")

def first(items: list[T]) -> T:
return items[0]


x: int = first([1, 2, 3]) # 类型检查器知道返回 int
s: str = first(["a", "b", "c"]) # 知道返回 str

约束泛型

1
2
3
4
5
6
7
8
9
10
from typing import TypeVar

Number = TypeVar("Number", int, float)

def double(x: Number) -> Number:
return x * 2

double(3) # int
double(3.14) # float
double("a") # ⚠️ 报错

Python 3.12+ 新语法(更简洁):

1
2
def first[T](items: list[T]) -> T:
return items[0]

十一、泛型类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from typing import Generic, TypeVar

T = TypeVar("T")

class Stack(Generic[T]):
def __init__(self) -> None:
self._items: list[T] = []

def push(self, item: T) -> None:
self._items.append(item)

def pop(self) -> T:
return self._items.pop()


s: Stack[int] = Stack()
s.push(1)
s.push("hi") # ⚠️ 报错

Python 3.12+:

1
2
class Stack[T]:
...

十二、dataclass 与类型注解

@dataclass 完美搭配注解——注解就是字段声明:

1
2
3
4
5
6
7
8
9
10
11
from dataclasses import dataclass

@dataclass
class User:
name: str
age: int
email: str = ""


u = User("咖飞", 3)
print(u.name) # IDE 补全

十三、装饰器与类型

装饰器保留类型信息需要一些技巧。简单的:

1
2
3
4
5
6
7
8
9
10
11
from typing import Callable, TypeVar
from functools import wraps

F = TypeVar("F", bound=Callable)

def log(func: F) -> F:
@wraps(func)
def wrapper(*args, **kwargs):
print(f"call {func.__name__}")
return func(*args, **kwargs)
return wrapper # 类型检查器认为返回类型和 func 一样

Python 3.10+ 的 ParamSpec 让参数类型精确传递:

1
2
3
4
5
6
7
8
9
from typing import ParamSpec, TypeVar, Callable

P = ParamSpec("P")
R = TypeVar("R")

def log(func: Callable[P, R]) -> Callable[P, R]:
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
return func(*args, **kwargs)
return wrapper

十四、mypy:静态类型检查器

安装:

1
pip install mypy

用:

1
2
mypy my_script.py
mypy src/

会报出所有类型不匹配。可以配置严格程度:

1
2
3
4
5
# pyproject.toml
[tool.mypy]
python_version = "3.12"
strict = true
warn_unused_ignores = true

类型注解只有配合 mypy(或 pyright/pylance)才有价值——单纯写注解不检查等于什么都没做。

十五、pyright / pylance

VS Code 的 Python 插件用 pylance(基于 pyright)实时检查类型——你写代码时红色波浪线就会立即出现。比 mypy 命令行更即时。

十六、TypeAlias 与 NewType

1
2
3
4
5
6
7
8
9
10
11
12
from typing import TypeAlias, NewType

# 别名
Vector: TypeAlias = list[float]

# NewType 是"逻辑上不同"的类型
UserId = NewType("UserId", int)

def get_user(uid: UserId) -> None: ...

get_user(123) # ⚠️ 是 int,不是 UserId
get_user(UserId(123)) # ✅

Python 3.12+ 新语法:

1
type Vector = list[float]

十七、Self(Python 3.11+)

方法里返回 self 类型:

1
2
3
4
5
6
7
8
9
from typing import Self

class Builder:
def add(self, x: int) -> Self:
self.items.append(x)
return self


b = Builder().add(1).add(2) # 链式,类型检查器知道每步返回 Builder

十八、注解的运行时用途

虽然默认不检查,但可以用 typing.get_type_hints 反射:

1
2
3
4
5
6
from typing import get_type_hints

def f(a: int, b: str) -> bool:
return True

print(get_type_hints(f)) # {'a': <class 'int'>, 'b': <class 'str'>, 'return': <class 'bool'>}

pydanticdataclassesFastAPI 都用这个来运行时校验数据。

十九、渐进式类型

Python 的类型注解是渐进式的——不用一次全部加上。策略:

  1. 先给公开 API 加(函数签名、类字段);
  2. 再给核心逻辑加
  3. 最后处理边缘代码
  4. 对无法确定的暂时用 Any

二十、类型注解的成本 vs 收益

成本:多打字、初期学习曲线。

收益

  • IDE 补全大幅提升;
  • 重构安全(改类型 mypy 立刻告诉你哪里坏);
  • 参数类型清晰,代码即文档;
  • 早发现 bug(None 传给不接受 None 的地方);
  • 团队协作更顺畅。

规则:任何超过 100 行的项目,都值得加类型注解。

二十一、一个完整例子:类型化的小工具库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# utils.py
"""一个类型注解齐全的工具库。"""

from __future__ import annotations

from collections.abc import Iterable, Callable
from typing import TypeVar, Protocol
from dataclasses import dataclass
from datetime import datetime


T = TypeVar("T")
K = TypeVar("K")


# ---------- 数据类 ----------

@dataclass
class Result[T]: # Python 3.12+ 泛型语法
ok: bool
value: T | None = None
error: str | None = None


# ---------- 协议 ----------

class Comparable(Protocol):
def __lt__(self, other: object) -> bool: ...


# ---------- 泛型工具 ----------

def first(items: Iterable[T], default: T | None = None) -> T | None:
for x in items:
return x
return default


def group_by[K, T](items: Iterable[T], key: Callable[[T], K]) -> dict[K, list[T]]:
result: dict[K, list[T]] = {}
for x in items:
result.setdefault(key(x), []).append(x)
return result


def find_max[T: Comparable](items: Iterable[T]) -> T | None:
it = iter(items)
try:
best = next(it)
except StopIteration:
return None
for x in it:
if best < x:
best = x
return best


# ---------- 使用 ----------

if __name__ == "__main__":
nums: list[int] = [3, 1, 4, 1, 5, 9, 2]
print(find_max(nums)) # 9
print(first([], default=-1)) # -1
print(group_by(["ap", "bc", "ad", "bb"], key=lambda s: s[0]))
# {'a': ['ap', 'ad'], 'b': ['bc', 'bb']}

二十二、常见陷阱

  1. 注解字符串(Forward Reference):类内部引用自己时要引号或 from __future__ import annotations
  2. 循环导入:注解触发的 import 陷入循环。放 if TYPE_CHECKING: 里。
  3. runtime 不检查:mypy 报错但代码还是能跑。别以为报了错就不能运行。
  4. Any 传染:一处 Any 会让检查失效一大片。
  5. 过度用泛型:小项目用不到。用得越多,读者理解成本越高。
  6. isinstance(x, list[int]):不能这么写——运行时 list[int] 不能用于 isinstance。用 isinstance(x, list)
  7. 注解与运行时行为不一致def f(x: int) -> None: 传字符串照跑不误——类型只是”建议”。

二十三、小结与延伸阅读

  • 类型注解是”运行时不检查、给工具看”的元数据;
  • 基础:intstrlist[X]dict[K, V]T | None
  • 泛型:TypeVarGeneric、3.12+ [T] 语法;
  • 复杂类型:CallableProtocolLiteralTypedDictNewType
  • @dataclass 时字段声明就是注解;
  • 装 mypy / 用 pylance 做静态检查;
  • 渐进式加,先公开 API,后内部;
  • 团队项目强烈推荐。

延伸阅读:


🎉 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 日