如何去使用python的typing.Annotated

在最近的开发中,我越来越多地使用 Pydantic 来进行字段校验。而在使用 Pydantic 的过程中,类型注解(type hint)几乎是必不可少的。其中一个经常出现的类型工具就是 typing.Annotated

起初,我在阅读 官方文档 时对它的用途感到困惑,看了半天也没搞懂它到底是做什么用的。后来在网上查阅了一些资料,终于理解了它的基本用途和背后的设计理念。于是,我决定将自己的理解总结下来,记录在这篇博客中,方便自己回顾,也希望能帮到同样疑惑的你。

Annotated 是什么?

在 Python 中,Annotated 允许开发者为类型声明附加元数据。语法如下:

1
2
# Annotated[T, x] 的含义是:这个变量是类型 T,同时附带了元数据 x。
var_name: Annotated[T, x]

来看一个简单的例子:

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

class A(TypedDict):
# 表示 name 是 str 类型,且 name[0] 应该是大写字母
name: Annotated[str, "first letter is capital"]

get_type_hints(A)
# 输出: {'name': <class 'str'>}

get_type_hints(A, include_extras=True)
# 输出: {'name': typing.Annotated[str, 'first letter is capital']}

get_type_hints(A, include_extras=True)['name'].__metadata__[0]
# 输出: 'first letter is capital'

可以看到,Annotated 本身并不会影响类型本身的行为,它的作用只是附加一些元数据。这些元数据可以通过 .__metadata__ 属性在运行时获取。但要真正“用起来”,还需要其他工具或框架来解析这些元数据并据此执行某些逻辑。

Annotated 在 Pydantic 和 FastAPI 中的应用

在使用 Pydantic 的框架中,Annotated 被广泛用于表达校验规则。例如在 FastAPI 中,你可以用它来传递额外的校验信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from typing import Annotated
from fastapi import Header, Query

# 参数 q 是 str 类型,最大长度为 50。
# 这个信息通过 Annotated 传递给 FastAPI,用于参数校验。
def read_items(q: Annotated[str, Query(max_length=50)]):
pass

# 用于依赖注入的情况也类似:
def UserIdHeader(alias: str | None = None, **kwargs):
if alias is None:
alias = "X-Forwarded-User"
return Header(alias=alias, **kwargs)

UserIdHeaderDep = Annotated[str | None, UserIdHeader()]

在 Pydantic 模型中定义校验规则

你还可以在 Pydantic 模型中使用 Annotated 来定义字段的校验规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from typing import Annotated
from pydantic import BaseModel, Field, field_validator

class Model(BaseModel):
x: int

@field_validator("x")
def between_2_and_20(cls, v: int) -> int:
if 2 < v < 20:
return v
raise ValueError(f"{v} is not between 2 and 20.")

class Model2(BaseModel):
# Annotated 提供的元数据 Field 可以直接用于字段验证
x: Annotated[int, Field(lt=20, gt=2)]

这是因为 Pydantic 内部能够解析 Annotated 中的 Field 并据此自动生成验证器,简化了手动定义校验逻辑的过程。

更高级的用法:函数作为元数据

Annotated 不仅可以添加静态信息,甚至可以附加函数。比如在 langgraph 库中就使用这种方式来实现动态行为。下面是一个简化示例:

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
from typing import Annotated, TypedDict, Callable, get_type_hints

def reducer(a: list, b: float) -> list:
return a + [b]

class State(TypedDict):
x: Annotated[list, reducer]

class Graph:
def __init__(self, schema: type):
self.metadata = {
name: typ.__metadata__[0]
for name, typ in get_type_hints(schema, include_extras=True).items()
}

def add_node(self, func: Callable) -> None:
self.func = func

def invoke(self, input_: dict[str, float]) -> dict[str, list[float]]:
output = {k: [v] for k, v in input_.items()}
for k, v in output.items():
processed = self.func(output)
output[k] = self.metadata[k](output[k], processed[k])
# ↑ 这里调用 reducer 函数处理结果
return output

graph = Graph(State)
graph.add_node(lambda x: {"x": x["x"][-1] + 1})
print(graph.invoke({"x": 0.5})) # 输出: {'x': [0.5, 1.5]}

这个例子展示了一个强大的用法:将函数作为元数据附加到类型上,在运行时动态处理字段的值。


总结

typing.Annotated 是 Python 类型系统中一个非常灵活的工具,它本身不会做任何“实事”,但提供了一种标准方式来附加元数据。只要配合能够解析它的框架或代码逻辑,就能实现非常强大的功能。在 Pydantic、FastAPI、LangGraph 等现代 Python 工具链中,它都发挥着关键作用。

希望这篇文章能帮你更好地理解并应用 Annotated,让你的类型注解既“好看”又“有用”。


如何去使用python的typing.Annotated
https://vegetablest.github.io/2025/05/28/如何去使用python的typing-Annotated/
作者
af su
发布于
2025年5月28日
许可协议