Python 类型注解(Type Hints)入门:驱动 FastAPI 声明式 API 的基石
Python 3 提供了可选的 type hints(类型注解) 语法,允许开发者在变量、函数参数与返回值上声明类型。本文档源自 FastAPI 官方文档中文翻译(docs/hi/docs/python-types.md,仓库内同内容文档覆盖多语言目录),它是学习 FastAPI 之前最重要的一篇基础指南——因为 FastAPI 完全构建在这些 type hints 之上:框架正是靠你声明的类型来完成请求数据解析、校验、转换、自动错误生成与 OpenAPI 文档生成。读完本文,你将掌握 Python 类型注解的核心语法(简单类型、泛型容器、Union、可空类型、类与 Pydantic 模型、Annotated 元数据),并理解它们如何在 FastAPI 的源码层面被转换为真实的 API 能力。
如果你已经是 Python 类型注解专家,可以直接跳过本页进入 Tutorial - User Guide(仓库中为 docs/hi/docs/tutorial/index.md)。
动机:一个没有类型注解的函数
先从最朴素的例子看起,这也是官方入门页的第一个示例,源码见 docs_src/python_types/tutorial001_py310.py:
def get_full_name(first_name, last_name):
full_name = first_name.title() + " " + last_name.title()
return full_name
print(get_full_name("john", "doe"))
调用这个程序输出:
John Doe
函数做的事情很简单:
- 接收
first_name与last_name两个参数; - 用
title()把每个参数的首字母转为大写; - 用中间的一个空格把它们拼接(concatenate)成一个字符串返回。
亲手编辑一下:糟糕的自动补全体验
这是一个非常简单的程序,但假设你是从零开始写它的:写到一半时,你需要在某个参数上调用"把首字母大写的方法"——那到底叫 upper?uppercase?first_uppercase?还是 capitalize?
你于是求助程序员的老朋友——编辑器的自动补全:敲下函数第一个参数 first_name,输入一个点号 .,再按 Ctrl+Space 触发补全。可惜的是,编辑器一无所知,弹出的列表里什么有用的东西都没有:
问题的根源在于:Python 是动态类型语言,first_name 此刻可以是任何东西——字符串、数字、对象,甚至一个函数。编辑器没有任何信息可以推断它的方法集。
添加类型注解(Type Hints)
现在只改上一版代码的一行:把函数参数列表从:
first_name, last_name
改成:
first_name: str, last_name: str
仅此而已。这就是 type hints 的全部入门形式。修改后的完整代码见 docs_src/python_types/tutorial002_py310.py:
def get_full_name(first_name: str, last_name: str):
full_name = first_name.title() + " " + last_name.title()
return full_name
print(get_full_name("john", "doe"))
需要注意,这与声明默认值完全是两回事。默认值写法是:
first_name="john", last_name="doe"
那会用等号 =;而类型注解使用的是冒号 :。并且,添加 type hints 通常不会改变代码原本的运行行为——它们只是元信息,运行时不产生强制约束。
再次回到"正在编写这个函数"的场景:这一次你带着类型注解写代码,同一时刻按下 Ctrl+Space,编辑器就能给出基于 str 的方法补全:
滚动浏览这些选项,很快就能找到那个"看起来眼熟"的正确方法(例如 capitalize):
更进一步:来自类型检查的错误预警
再看一个已经带类型注解的函数,完整源码见 docs_src/python_types/tutorial003_py310.py:
def get_name_with_age(name: str, age: int):
name_with_age = name + " is this old: " + age
return name_with_age
因为编辑器知道了每个变量的类型,它不仅能补全,还能做类型检查。此处 name 是 str,age 是 int,直接把两者用 + 拼接显然会出问题,编辑器会立刻标红提示:
现在你明白该修复它了——把 age 用 str(age) 转成字符串。修复后的版本见 docs_src/python_types/tutorial004_py310.py:
def get_name_with_age(name: str, age: int):
name_with_age = name + " is this old: " + str(age)
return name_with_age
在哪里声明类型
你刚刚看到了 type hints 最主要的声明位置——函数参数。这也是之后在 FastAPI 中几乎唯一会用到的位置。除此之外,类型注解同样可以用于模块级变量、类属性等,语法完全一致。
简单类型
除了 str,所有标准 Python 类型都可以直接使用,例如:
intfloatboolbytes
官方示例见 docs_src/python_types/tutorial005_py310.py,它在同一函数里混合使用了全部四种:
def get_items(item_a: str, item_b: int, item_c: float, item_d: bool, item_e: bytes):
return item_a, item_b, item_c, item_d, item_e
这些简单类型恰好与 FastAPI 路由参数的基础校验一一对应:整数自动解析与校验、浮点、布尔、字节串,以及 OpenAPI 中对应的 schema 类型。
typing 模块
对于某些额外的使用场景,需要从标准库的 typing 模块导入一些辅助类型。比如想声明"参数可以是任意类型",就使用 typing.Any:
from typing import Any
def some_function(data: Any):
print(data)
注意 Any 会关闭该处的类型推断,编辑器不会再对该变量给出类型相关支持,属于必要时才使用的逃生通道。
泛型类型(Generic Types)
有些类型可以接收"写在方括号里的类型参数",用来定义其内部元素的类型。例如"由字符串组成的 list"应该声明为 list[str]。
这类可以接收类型参数的类型被称为 Generic types(泛型) 或 Generics。Python 内置类型可以原样当泛型使用(外面一层类型 + 方括号内元素类型):
listtuplesetdict
仓库中对应的系列示例源码集中在 docs_src/python_types/ 目录下(均为 Python 3.10+ 语法)。
List:字符串列表
先定义一个"str 的 list"类型的变量。声明变量用同样的冒号 : 语法;类型写成 list;由于 list 内部还有元素类型,把这些内部类型放进方括号:
def process_items(items: list[str]):
for item in items:
print(item)
(完整文件:docs_src/python_types/tutorial006_py310.py)
说明:方括号里的这些内部类型被称为 type parameters(类型参数)。本例中
str就是传给list的类型参数。
它的含义是:"变量 items 是一个 list,且其中每个元素都是 str"。这样声明之后,编辑器连遍历过程中的 item 都能给出正确支持:
![在没有额外约束的情况下,编辑器通过 list[str] 推断出 item 也是 str 并给出方法补全](https://raw.gitcode.com/GitHub_Trending/fa/fastapi/files/master/docs/en/docs/img/python-types/image05.png)
注意这里的变量 item 只是 items 里的一个元素,编辑器却已经知道它是 str 并针对它给出提示——在没有类型注解的情况下,这一点几乎不可能实现。同样的推断在 FastAPI 中意义重大:声明 items: list[str] 的请求体,框架会自动逐元素校验并转换。
Tuple 与 Set
tuple 与 set 的声明方式类似,见 docs_src/python_types/tutorial007_py310.py:
def process_items(items_t: tuple[int, int, str], items_s: set[bytes]):
return items_t, items_s
这段代码的含义是:
- 变量
items_t是一个包含 3 个元素的tuple,三个元素依次是int、int与str——元组的类型参数逐个对应每个位置的元素类型; - 变量
items_s是一个set,其中每个元素的类型是bytes。
Dict:键值对都要声明
声明 dict 需要传递 两个类型参数,用逗号分隔:第一个描述 dict 的 key 类型,第二个描述 value 类型。示例见 docs_src/python_types/tutorial008_py310.py:
def process_items(prices: dict[str, float]):
for item_name, item_price in prices.items():
print(item_name)
print(item_price)
含义是:
- 变量
prices是一个dict:- 它的 key 是
str类型(例如每件商品的名称); - 它的 value 是
float类型(例如每件商品的价格)。
- 它的 key 是
FastAPI 正是靠这种容器注解来决定请求体 / 响应中嵌套结构的 schema,例如 dict[str, float] 会反映为 OpenAPI 中的 additionalProperties 约束。
Union:多类型联合
你可以声明一个变量可能是多种类型之一,例如既可能是 int 也可能是 str。定义方法是把两种类型用竖线 |(vertical bar,也叫 "bitwise or operator",但此处与位运算语义无关)分隔。因为变量可以属于这两个类型集合的并集,所以它叫 union。示例见 docs_src/python_types/tutorial008b_py310.py:
def process_item(item: int | str):
print(item)
即:item 可以是 int 或 str。
可能为 None 的可空类型
还可以声明某个值(比如 str)可能为 None。Python 3.10+ 写法见 docs_src/python_types/tutorial009_py310.py:
def say_hi(name: str | None = None):
if name is not None:
print(f"Hey {name}!")
else:
print("Hello World")
把单纯的 str 换成 str | None,编辑器就能帮你捕捉"想当然认为它永远是 str,实际却可能是 None"的那类错误。在 FastAPI 中,str | None = None 意味着该参数可选:传了按 str 校验,不传则为 None,而 str | None(无默认值)则一般表示"可传 null 但必须显式提供"。
把类当作类型使用
你同样可以把一个类声明为变量的类型。假设有一个带 name 属性的类 Person,见 docs_src/python_types/tutorial010_py310.py:
class Person:
def __init__(self, name: str):
self.name = name
def get_person_name(one_person: Person):
return one_person.name
于是就可以把变量声明为 Person 类型,随后享受完整的编辑器支持——自动补全会列出 Person 的属性与方法(例如 name):
要特别强调的是,one_person: Person 的意思是"one_person 是类 Person 的一个实例",而不是"one_person 是那个名为 Person 的类本身"。
Pydantic Models:用类描述数据形状
Pydantic 是一个用于数据校验的 Python 库:你先把数据的"形状"声明为带属性的类,每个属性都标有类型;然后用一组值创建该类的实例,Pydantic 就会校验这些值、必要时把它们转换成正确的类型,最终返回一个包含完整数据的对象——用这个对象时你同样拥有完整的编辑器支持。
Pydantic 是 FastAPI 的数据校验基础,二者深度绑定,仓库内 FastAPI 的请求体解析正是通过将 body 数据实例化为 Pydantic 模型对象来完成的。
取自 Pydantic 官方文档的典型示例(仓库版本见 docs_src/python_types/tutorial011_py310.py):
from datetime import datetime
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str = "John Doe"
signup_ts: datetime | None = None
friends: list[int] = []
external_data = {
"id": "123",
"signup_ts": "2017-06-01 12:22",
"friends": [1, "2", b"3"],
}
user = User(**external_data)
print(user)
# > User id=123 name='John Doe' signup_ts=datetime.datetime(2017, 6, 1, 12, 22) friends=[1, 2, 3]
print(user.id)
# > 123
注意观察这里发生的自动转换与校验:
id声明为int,传入字符串"123"被自动转成了整数123;signup_ts声明为datetime | None,字符串"2017-06-01 12:22"被解析成了datetime对象;friends声明为list[int],传入的[1, "2", b"3"]被逐元素校验并统一转换成[1, 2, 3]。
如果传入的数据与声明的类型完全不匹配(例如 id 传了 "abc"),Pydantic 会抛出校验错误。FastAPI 在此基础上更进一步:在 Web 请求/响应边界触发同样的校验流程,并把失败信息封装成带 422 状态码的标准错误响应返回给客户端。
带元数据注解的类型提示(Annotated)
Python 还有一个特性,允许通过 Annotated 在类型注解里附带额外的元数据(关于数据的数据,例如对类型的描述说明)。从 typing 中导入 Annotated 即可使用,示例见 docs_src/python_types/tutorial013_py310.py:
from typing import Annotated
def say_hello(name: Annotated[str, "this is just metadata"]) -> str:
return f"Hello {name}"
Python 自身不会对 Annotated 做任何特殊处理;对编辑器和各类工具而言,这里的类型仍然是 str。但你可以把 Annotated 的位置利用起来,给 FastAPI 传递额外的元数据,从而定制应用的行为。
需要牢记的关键点是:传给 Annotated 的第一个类型参数才是真正的类型,其余部分对其他工具而言只是元数据。在源码层面,FastAPI 的 analyze_param 函数 正是用 typing.get_origin 判断注解是否为 Annotated,然后取出第一个类型参数作为实际类型,并从剩余参数中识别 FieldInfo、Depends 等 FastAPI 专属注解。
之所以强调"这是标准 Python",意味着在你的编辑器、代码分析与重构工具里,你依然能得到尽可能好的开发体验,同时你的代码也能与大量其他 Python 工具与库保持高度兼容。
现在你只需要知道 Annotated 是标准 Python 语法、它存在即可;后续章节你会看到它到底有多强大。
FastAPI 如何利用 Type Hints
FastAPI 正是利用这些 type hints 完成了大量工作。当你用 type hints 声明参数后,你获得的是:
- 编辑器支持(Editor support);
- 类型检查(Type checks)。
与此同时,FastAPI 复用同一份声明,自动替你完成:
- 定义需求:从请求中提取 path 参数、query 参数、headers、body、dependencies 等的要求;
- 转换数据:把请求数据转换成所需的目标类型;
- 校验数据:校验每个请求带来的数据,并在数据不合法时生成自动错误返回给客户端;
- 文档化 API:基于这些类型生成 OpenAPI schema,再被自动交互式文档 UI(Swagger UI / ReDoc)使用。
以上听起来可能有些抽象,但不用担心,在 Tutorial - User Guide 中你会看到这一切的实际运行。
源码视角:类型注解如何变成 API 能力
为了理解"复用同一份声明"究竟如何实现,可以看一下框架的依赖分析核心 fastapi/dependencies/utils.py:
- get_typed_signature 用
inspect.Signature读取路由处理函数的参数签名。对于以字符串形式出现的注解(例如启用了 PEP 563 延迟求值的情形),先交由ForwardRef处理,再调用evaluate_forwardref求值成真实的类型对象——该能力在 fastapi/_compat/v2.py 中针对 Pydantic v2 实现; - analyze_param 对每个参数做类型分析:先剥离
Annotated(如上文所述取第一个类型参数),再识别参数来源于Param/Body/Depends等,并处理默认值语义。也就是说,一个item_id: int这样的普通注解,就能让 FastAPI 推断出它来自哪类请求位置、需要何种校验。
这正应了文档的结论:坚持使用标准 Python 类型、在同一个位置声明,而不需要额外的类或装饰器,FastAPI 就能替你完成大量工作。如果你在学完整个教程后想回顾更多类型知识,官方文档也推荐参考 mypy 提供的类型注解速查表(cheat sheet)作为补充资源。
小结
| 主题 | 语法要点 | 仓库示例 |
|---|---|---|
| 函数参数注解 | first_name: str |
tutorial002 |
| 简单类型 | int / float / bool / bytes |
tutorial005 |
| 任意类型 | from typing import Any |
正文代码 |
| 列表 | list[str] |
tutorial006 |
| 元组与集合 | tuple[int, int, str] / set[bytes] |
tutorial007 |
| 字典 | dict[str, float] |
tutorial008 |
| 联合类型 | int | str |
tutorial008b |
| 可空类型 | str | None |
tutorial009 |
| 类作为类型 | one_person: Person |
tutorial010 |
| Pydantic 模型 | class User(BaseModel) |
tutorial011 |
| 带元数据注解 | Annotated[str, ...] |
tutorial013 |
以上示例的源码均存放于 docs_src/python_types/ 目录(注意官方教程示例采用 Python 3.10+ 语法,这也对应 FastAPI 当前文档主线所要求的最低 Python 版本)。掌握这些基础后,下一步即可进入 Tutorial - User Guide,亲身实践"一份类型注解同时换来校验、转换、自动错误与文档"的 FastAPI 开发方式。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00




