首页
/ FastAPI 中的 Python 类型标注实践指南:从基础注解、泛型到 Annotated 元数据与 Pydantic 模型

FastAPI 中的 Python 类型标注实践指南:从基础注解、泛型到 Annotated 元数据与 Pydantic 模型

2026-09-06 21:13:05作者:卓艾滢Kingsley

本文基于 FastAPI 官方文档的「Python 类型」章节(德语版 docs/de/docs/python-types.md)展开。文章先系统讲解 Python 类型标注(type hints)的最小必要知识——基础类型、泛型、联合类型、类作为类型、Pydantic 模型与 Annotated 元数据,再结合 FastAPI 源码剖析这些类型标注是如何被框架在请求参数定义、数据转换、数据校验和 OpenAPI 文档生成中实际消费的。读完本文,你可以独立读懂并写出 FastAPI 所需的类型声明,并理解其在框架底层的完整处理链路。

如果你是已经熟练掌握类型标注的 Python 老手,可以直接跳到 FastAPI 如何利用类型标注 一节,其余章节可以快速略读。

为什么需要类型标注:从编辑器补全到自动纠错

Python 支持可选的「类型指示」(type hints,也称类型标注),这是一种允许你声明变量类型(例如 strintfloatbool)的特殊语法。声明类型后,编辑器和静态工具就能提供更好的支持。这是一份面向 FastAPI 使用的最小知识速览——FastAPI 完全建立在这些类型标注之上,即便你从未使用过 FastAPI,了解它们同样有价值。

先看一个不带类型标注的简单例子,对应 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_namelast_name 两个参数;
  • 利用 title() 把每个单词的首字母转为大写;
  • 用中间的空格将它们拼接在一起。

现在想象你正在自己写这个函数:参数已经定义好,接下来要调用「那个把首字母转成大写的方法」——它是 upperuppercasefirst_uppercase?还是 capitalize?于是你尝试编辑器补全:输入 first_name,敲一个点(.),按下 Ctrl+Space 触发自动补全……由于编辑器不知道 first_name 是什么类型,你只会得到一个空白的、毫无用处的候选列表。

加上类型标注

只需修改上一版本中的一行。把函数参数从:

    first_name, last_name

改为:

    first_name: str, last_name: str

就这样。这就是「类型标注」,完整版本见 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"

这是两件不同的事:类型标注使用冒号(:,而不是等号(=)。而且添加类型标注通常不会改变代码的运行时行为。

但回到编写这个函数的场景,这次你带着类型标注。在同一位置按下 Ctrl+Space 触发补全时,编辑器因为知道 first_namestr,会列出字符串的全部可用方法,你可以上下翻页,直到找到「对的就是它」的那个方法(title)。

更多的动机:类型错误检查

再看一个已经带类型标注的函数,对应 tutorial003_py310.py

def get_name_with_age(name: str, age: int):
    name_with_age = name + " is this old: " + age
    return name_with_age

由于编辑器知道每个变量的类型,你得到的不仅是代码补全,还有错误检查str + int 在 Python 中会失败,因此编辑器/类型检查器会在这里标黄提醒你。既然你知道要修复它,就用 str(age)age 转换为字符串,修复后见 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

声明类型

上面你已经看到了类型标注最主要的使用场景:函数参数。在 FastAPI 中,类型标注也主要用在函数参数上。

简单类型

你可以声明所有内置 Python 类型,而不仅仅是 str,例如 intfloatboolbytes,对应 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

typing 模块

对于一些额外场景,你可能需要从标准库 typing 模块导入内容。例如,如果你要声明某个参数可以是「任意类型」,可以使用 typing 中的 Any

from typing import Any


def some_function(data: Any):
    print(data)

泛型类型(Generic Types)

有些类型可以在方括号内接受「类型参数」,用以定义其内部元素类型。例如「一个字符串列表」要声明为 list[str]。这类能接受类型参数的类型被称为泛型类型(Generics)。

内置的这些类型都可以作为泛型使用(方括号 + 内部类型):

  • list
  • tuple
  • set
  • dict

列表(list)

定义一个变量,让它是一个由 str 组成的 list——即一个字符串列表。使用同样的冒号语法声明变量;类型取 list;由于列表是「内部包含类型」的容器,内部类型用方括号包起来。对应 tutorial006_py310.py

def process_items(items: list[str]):
    for item in items:
        print(item)

方括号内的内部类型被称为类型参数。在这里 str 就是传给 list 的类型参数。

这句话的含义是:「变量 items 是一个 list,并且这个列表中的每个元素都是 str」。有了这个声明,当你遍历列表处理其中的元素时,编辑器甚至能针对列表项提供字符串方法补全。

注意:变量 item 是列表 items 中的一个元素。尽管它只是循环变量,编辑器依然知道它是 str,并提供相应的补全支持。没有类型标注时,几乎不可能做到这一点。

元组(tuple)与集合(set)

同理,可以声明一个元组和集合,对应 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的类型,第二个是的类型,对应 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
    • 键是 str 类型(比如各个商品的名称);
    • 值是 float 类型(比如每件商品的价格)。

联合类型(Union)

你可以声明一个变量可以是多种类型之一,比如 intstr。使用**竖线(|)**分隔两个类型(它也是「按位或运算符」,但这里的含义与此无关),对应 tutorial008b_py310.py

def process_item(item: int | str):
    print(item)

这表示 item 可以是 int,也可以是 str。之所以叫「Union(联合)」,是因为该变量的值可以来自这两个类型集合的并集。

可能为 None

你还可以声明一个值可以是某个类型(如 str),但也可能是 None,对应 tutorial009_py310.py

def say_hi(name: str | None = None):
    if name is not None:
        print(f"Hey {name}!")
    else:
        print("Hello World")

当你在参数声明中使用 str | None 而不是只用 str 时,编辑器会帮助你发现那些「假设某个值永远是 str、但它实际上可能是 None」的错误。这在 FastAPI 中非常常见——例如可选的查询参数、可选的表单字段都会以 X | None 的形式声明。

把类作为类型

你也可以把声明为变量类型。假设你有一个带名字属性的 Person 类,对应 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 类型的变量。此时你会再次获得完整的编辑器支持:输入 one_person. 时,补全列表里会有 name 属性。

注意,one_person: Person 的含义是:「one_personPerson 类的一个实例」,而不是「one_person 就是名为 Person本身」。

Pydantic 模型:声明数据的「形状」并自动校验

Pydantic 是 Python 的数据校验库(原文此处链接至 Pydantic 官方文档)。你用带类型属性的类来声明数据的「形状」(form),每个属性都有一个类型。然后你用一些值创建这个类的实例,Pydantic 会校验这些值、必要时把它们转换为合适的类型,最终给你一个携带全部数据的对象——并且你还能获得针对这个对象的完整编辑器支持。

官方文档中的一个经典示例,对应 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" 是字符串 "123",Pydantic 自动转换成了 int 类型的 123
  • "signup_ts" 是字符串形式的日期时间,被解析成了 datetime.datetime(2017, 6, 1, 12, 22)
  • friends 中的 "2"b"3" 都被转换成了 int,最终是 [1, 2, 3]
  • 缺少必填性存疑的 name 字段时使用了默认值 "John Doe"

FastAPI 完全基于 Pydantic 构建。你在 FastAPI 中定义的请求体模型、response_model、依赖注入等,底层走的都是这套「声明类型 → 校验并转换数据」的机制。更多关于 Pydantic 的用法可以查阅其官方文档(此处不展开外部链接)。

使用 Annotated 在类型标注中嵌入元数据

Python 还提供了在类型标注中携带额外元数据(metadata)的能力,工具就是 typing 模块的 Annotated,对应 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 提供关于「你的应用应该如何工作」的元数据,例如:参数描述、取值范围(GeLe 等)、Query/Path/Header 的别名与默认值等。

关键规则:传给 Annotated第一个类型参数实际的类型,其余参数都是供其他工具使用的元数据。

现阶段你只需要知道 Annotated 存在,而且它是标准 Python 的一部分。这一点非常重要:

  • 因为它是标准 Python,你在编辑器里始终能获得最好的开发体验,配合你用来分析、重构代码的各类工具都能正常识别;
  • 同时你的代码与大量其他 Python 工具和库保持了很高的兼容性。

在后续章节(路径参数、查询参数、请求体、依赖注入等)中,你会反复看到 Annotated 如何被用来声明参数约束和元数据,它是 FastAPI 现代写法的核心语法。

FastAPI 如何利用类型标注(源码级解析)

FastAPI 利用这些类型标注来做多件事。当你用 FastAPI 声明带类型标注的参数时,你首先获得的是:

  • 编辑器支持(补全);
  • 类型检查(静态错误提示)。

FastAPI 会使用同样的声明来:

  • 定义请求:从路径参数、查询参数、Header、Body、依赖等中解析请求;
  • 转换数据:把请求中的原始数据(字符串、表单值等)转换为你声明的类型;
  • 校验数据:对每个请求做数据校验,当数据无效时自动生成错误响应并返回给客户端;
  • 用 OpenAPI 文档化 API:生成的 OpenAPI 文档被自动生成的交互式文档界面(Swagger UI 等)使用。

听起来可能比较抽象,但在 教程 – 用户手册 中你会看到所有这些在实际中是如何运作的。最重要的是:FastAPI 通过在单一位置使用标准 Python 类型(而不是引入额外的类、装饰器等)就完成了大部分繁重的工作。

源码证据:从函数签名到参数解析

这些能力在 FastAPI 源码中有一条清晰的调用链可以印证:

  1. 读取带类型的函数签名——fastapi/dependencies/utils.py 中的 get_typed_signature() 通过 inspect.signature() 拿到可调用对象的签名,并对每个参数调用 get_typed_annotation()fastapi/dependencies/utils.py#L230-L236)把字符串形式的注解解析为真实类型对象(含前向引用 ForwardRef 的处理)。这正是「你在函数参数上写的 strlist[int]Annotated[...]」被框架读取的入口。

  2. 逐参数分析——fastapi/dependencies/utils.py 中的 get_dependant() 遍历 endpoint_signature.parameters,对每个参数判断它是否出现在路径模板中(is_path_param = param_name in path_param_names),然后调用 analyze_param() 结合注解(annotation=param.annotation)和默认值(value=param.default)决定这个参数是路径参数、查询参数、Header、Body 还是依赖函数(param_details.depends)。这就是「同一份类型标注同时服务于路径参数、查询参数、请求体、依赖」的实现基础。

  3. 递归解析依赖——get_dependant() 还会对依赖函数继续递归调用自身(fastapi/dependencies/utils.py#L322-L331),因此依赖链上每一层的类型标注都会被同样解析、校验和文档化。

从源码结构看,FastAPI 并没有为每个「参数类别」设计独立 API,而是统一依赖 Python 类型系统 + 默认值 + Annotated 元数据这一个声明面,再在 analyze_param() 内部做路由与解析决策。这也解释了为什么文档强调「只需声明一次类型,就能同时获得补全、校验和 OpenAPI 文档」。

运行时行为与文档生成

  • 数据转换与校验:请求数据先经过 Pydantic 模型/类型约束的解析,字符串形式的 intfloat、日期等会被自动转换,非法值会生成结构化的 422 校验错误返回给客户端——这与前文 Pydantic 示例中 "123"123、日期字符串 → datetime 的行为是同一套机制。
  • OpenAPI 文档:同样的类型信息会被写入 OpenAPI 的 components/schemas 与参数定义中,供 /docs 交互式文档界面渲染。仓库中 docs_src/ 下每个教程目录(如 docs_src/python_types/)对应的 tutorial0XX_py310.py 都可运行查看实际效果。

小结

  • 类型标注的最小集合:str/int/float/bool/bytes 等内置类型;typing 模块的 Any;泛型 list[...]tuple[...]set[...]dict[...];联合类型 int | str 与可空类型 str | None;以及把自定义类(含 Pydantic 模型)直接作为类型使用。
  • Annotated[实际类型, 元数据...] 是标准 Python 语法,FastAPI 用其中「第一个参数之外的位置」接收描述、校验约束等元数据。
  • FastAPI 在 fastapi/dependencies/utils.py 中通过 get_typed_signature()get_dependant()analyze_param() 这条链路读取这些标注,统一实现请求定义、数据转换、数据校验与 OpenAPI 文档生成。
  • 若想在本文之外继续深入类型标注,官方文档建议参考 mypy 的 Cheat Sheet(作为类型标注语法的补充速查)。

完整的使用场景(路径参数、查询参数、请求体、依赖注入等)请继续阅读 教程 – 用户手册

登录后查看全文
热门项目推荐
相关项目推荐