FastAPI-基本入门
这篇讲什么
- FastAPI 是「类型注解驱动」的 ASGI 框架:路由来自 Starlette,校验和序列化来自 Pydantic,剩下的核心机制就是依赖注入。这篇先把环境、第一个应用、三种启动方式和工程骨架跑通。
- 最重要的一条实测结论:
async def里写阻塞调用比def里写更糟。同一个 0.4 秒的time.sleep,def版本四个并发 0.42 秒、async def版本 1.61 秒,而且期间夹一个普通请求要等 1.21 秒。 - 环境基线是 FastAPI 0.162.0 + Starlette 1.10.0 + Pydantic 2.14.3 + uvicorn 0.54.0(conda
fastapi环境,Python 3.14.10),文中每条断言都在本机跑过。
FastAPI 是什么
-
官方首页给的定义是三个词:high performance、easy and fast to learn、based on standard Python type hints。它不打算做全功能框架,而是把「写接口」这条路径上的三件事做到自动:参数校验、序列化、文档生成。
-
所以 FastAPI 其实是三层拼起来的,看清这层关系,后面所有行为的来源都好解释:
- 路由与 ASGI 协议由 Starlette 提供(实测
starlette==1.10.0),Request、Response、WebSocket、中间件这些名字都能从fastapi直接导入,本质是 Starlette 的。 - 数据校验与序列化由 Pydantic v2 提供(实测
pydantic==2.14.3,底层是 Rust 写的pydantic-core==2.50.4)。 - 依赖注入是 FastAPI 自己写的那层,靠函数签名的类型注解工作,是它区别于其它框架最核心的机制。
- 路由与 ASGI 协议由 Starlette 提供(实测
-
「ASGI」是理解 FastAPI 的关键。WSGI 是同步协议,一个 worker 一次只能处理一个请求;ASGI 是异步协议,同一个进程里可以挂着成千上万个还没完成的连接。对照关系:
- Flask、Django 的传统写法是 WSGI,要靠多进程或多线程堆并发(见 Flask-项目部署)。
- Tornado 自带异步 HTTP 服务器,不需要外面再套一层(见 Tornado-基本入门)。
- FastAPI 只是框架,自己不监听端口,必须配一个 ASGI 服务器,官方默认是 uvicorn。
-
和本仓库里另外三套的分工,可以按这张表记:
框架 定位 自带什么 要自己选的 Django 全功能,后台与 ORM 齐全 ORM、admin、模板、迁移、认证 基本什么都不用选 Flask 微框架,靠扩展拼装 路由 + 模板(Jinja2) ORM、校验、认证全靠自己选 Tornado 异步框架 + 自带 HTTP 服务器 IOLoop、AsyncHTTPClient、WebSocket ORM、模板、校验 FastAPI 异步 API 框架 校验(Pydantic)、文档(OpenAPI)、依赖注入 服务器(uvicorn)、ORM、模板 一句话:做管理后台选 Django,做接口服务选 FastAPI,长连接/事件驱动的老项目才考虑 Tornado,Flask 适合小工具和已有生态。
-
FastAPI 不自带 ORM 和模板引擎,但两者都能接:SQLAlchemy 的接法见 FastAPI-数据库;要页面而不是 JSON 时用
Jinja2Templates,写法与 Flask 同构,但调用形式换了,见下一节。
安装与环境
-
官方推荐的装法是一条命令:
1
2
3conda create -n fastapi python=3.14 -y
conda activate fastapi
pip install "fastapi[standard]"- 引号别省:
fastapi[standard]里的方括号在 zsh 下会被当成通配符去匹配文件,直接报no matches found。 [standard]是 extra(附加依赖集),不带它只会装到「框架本体」,fastapi这个命令行工具和 uvicorn 都没有。
- 引号别省:
-
本机实测装完之后(
pip list节选,Python 3.14.10):1
2
3
4
5
6
7
8
9
10
11
12
13
14fastapi 0.162.0
starlette 1.10.0
pydantic 2.14.3
pydantic-core 2.50.4
uvicorn 0.54.0
fastapi-cli 0.0.32
httpx 0.28.1
anyio 4.16.0
websockets 17.4
jinja2 3.1.6
python-multipart 0.0.33
email-validator 2.3.0
pydantic-settings 2.16.2
fastar 0.12.0 -
standard这个 extra 到底带了什么,直接读元数据比背清单可靠:1
python -c "from importlib.metadata import metadata; r=metadata('fastapi').get_all('Requires-Dist'); print(len([x for x in r if 'standard' in x]),'条 standard 依赖'); print([x for x in r if ';' not in x])"
1
2
3
4standard extra 条数: 25
主依赖: ['starlette>=0.46.0', 'pydantic>=2.9.0', 'typing-extensions>=4.8.0',
'typing-inspection>=0.4.2', 'annotated-doc>=0.0.2', 'opentelemetry-api>=1.44.0']
有没有 orjson: False | 有没有 fastar: True-
注意最后两行:新版
standard已经不装orjson了,换成了 Rust 实现的fastar。网上大量教程让你pip install orjson配ORJSONResponse,这条现在双重过时——类还能导入,但一用就:1
2
3
4
5from fastapi.responses import ORJSONResponse # 能导入
ORJSONResponse({"a": 1})
-> FastAPIDeprecationWarning: ORJSONResponse is deprecated, FastAPI now serializes
data directly to JSON bytes via Pydantic when a return type or response model is set
-> AssertionError: orjson must be installed to use ORJSONResponse也就是说框架已经直接走 Pydantic 的 Rust 序列化,不需要自定义响应类;
ORJSONResponse属于「官方明确弃用」的写法,别再装 orjson 去配它。 -
主依赖里出现了
opentelemetry-api,这是 0.162 新加的:FastAPI 现在原生支持 OpenTelemetry,构造参数里就有telemetry=(详见 FastAPI-响应模型与OpenAPI文档那篇末尾)。
-
-
Python 版本要求,实测元数据:
1
2
3fastapi Requires-Python =>=3.10
starlette Requires-Python =>=3.11
pydantic-settings Requires-Python =>=3.11也就是说虽然 FastAPI 本体写的是 3.10,但它依赖的 Starlette 1.x 要求 3.11+,实际下限是 3.11。
-
只装
pip install fastapi会怎样?命令行工具直接拒绝启动,而且提示写得很明确:1
2
3
4
5To use the fastapi command, please install "fastapi[standard]":
pip install "fastapi[standard]"
RuntimeError -
装第三方包之前先看它的依赖上限,
pip降级核心库是不吭声的(本机在 Django 那边踩过)。装 FastAPI 生态包时同样适用:1
pip install --dry-run "sqlalchemy>=2" alembic aiomysql asyncmy asyncpg mysqlclient
1
2Would install SQLAlchemy-2.1.8 alembic-1.20.1 aiomysql-0.3.2 asyncmy-0.2.17
asyncpg-0.32.0 mysqlclient-2.3.0 PyMySQL-1.2.3确认
Would install里没有fastapi、pydantic、starlette的旧版本再真装,说明它没有把核心包往下拽。
第一个应用
-
最小可运行版本,
main.py:main.py 1
2
3
4
5
6
7
8from fastapi import FastAPI
app = FastAPI()
async def root():
return {"hello": "world"} -
起服务有三条路,任选一条就能跑起来(区别在下一节讲):
1
2
3
4
5
6# 一、直接用 uvicorn(参数最全)
python -m uvicorn main:app --host 127.0.0.1 --port 8000
# 二、官方 CLI 的开发模式(自动 reload,默认只听 127.0.0.1)
fastapi dev main.py --port 8000
# 三、官方 CLI 的生产模式(默认听 0.0.0.0、不 reload)
fastapi run main.py两种起法的实测日志:
1
2
3
4
5
6
7
8
9
10
11
12python -m uvicorn main:app ...
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: Application startup complete.
fastapi dev main.py --port 8000
Starting FastAPI in development mode
Using import string: main:app
Server started at http://127.0.0.1:8000
Documentation at http://127.0.0.1:8000/docs
INFO: Started reloader process [46196] using WatchFiles
INFO: Started server process [41504]
INFO: Application startup complete. -
不想起服务器也能验,
TestClient直接在进程内发请求:test_first.py 1
2
3
4
5
6
7from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
resp = client.get("/")
print(resp.status_code, resp.json(), "|", resp.headers.get("content-type"))1
200 {'hello': 'world'} | Content-Type: application/json
-
三个第一眼就会疑惑的点,实测过:
return dict不用手动转 JSON,FastAPI 直接序列化并把Content-Type设成application/json。这点和 Flask 3 的return {...}行为一致(见 Flask-Restful API 开发),和 Tornado 要自己self.write(dict)不同。operationId是自动拼出来的,不是函数名。实测def get_user(uid: int)挂在/users/{uid}上,生成的operationId是get_user_users__uid__get(函数名 + 路径 + 方法,非字母换成下划线)。要固定它就在装饰器里传operation_id="myGoods",实测文档里就变成myGoods;函数名仍然建议按 PEP8 起(get_user_list而不是getUserList)。async def和def都能用,但语义完全不同,这是 FastAPI 最容易写错的地方,单独开一节讲。
渲染 HTML(Jinja2Templates)
-
挂模板与静态目录,实测:
pages.py 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17import os
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
BASE = os.path.dirname(os.path.abspath(__file__))
app = FastAPI()
templates = Jinja2Templates(directory=os.path.join(BASE, "templates"))
app.mount("/static", StaticFiles(directory=os.path.join(BASE, "static")), name="static")
def page(request: Request):
return templates.TemplateResponse(request, "home.html", {"title": "主页", "name": "lilei"})templates/home.html 1
<h1>{{ title }}</h1><p>{{ name }}</p>
1
2GET /page -> 200 <h1>主页</h1><p>lilei</p>
GET /static/site.css -> 200 text/css; charset=utf-8 -
⚠️ 老教程里的调用形式
TemplateResponse("home.html", {"request": request, ...})在 Starlette 1.x 上直接 500,实测报:1
TypeError: cannot use 'tuple' as a dict key (unhashable type: 'dict')
现在只有两种写法可用(实测都 200):位置参数
TemplateResponse(request, name, context),或全关键字TemplateResponse(request=request, name=..., context=...)。签名实测是:1
2TemplateResponse(request, name, context=None, status_code=200,
headers=None, media_type=None, background=None)也就是说
request挪到了第一个参数,不用再塞进 context 里。 -
⚠️ 跑这个例子前先确认自己在跑哪个 app。
pages.py里又app = FastAPI()建了一个新的应用对象,它和main.py里那个没有任何关系,fastapi dev main.py起来的服务里就是没有/page。实测同一个目录、同一批文件,两种起法的结果正好相反:1
2
3
4
5
6
7fastapi dev main.py -> Using import string: main:app
GET / 200 {"message":"Hello World"}
GET /page 404 {"detail":"Not Found"}
fastapi dev pages.py -> Using import string: pages:app
GET / 404 {"detail":"Not Found"}
GET /page 200 <h1>主页</h1><p>lilei</p>- 起服务时终端第二行
Using import string: xxx:app就是答案:它写哪个模块,路由就只去那个模块里找。/正常而/page404,多半就是这个原因。 - 想两个文件一起生效就别建第二个 app:把
pages.py改成APIRouter,由main.py用include_router挂上去,写法见下面「工程骨架」。
- 起服务时终端第二行
-
⚠️
static/目录必须在导入之前就存在。StaticFiles在构造时就检查目录,目录没有会让整个模块导入失败,服务压根起不来:1
RuntimeError: Directory '...\static' does not exist
这行就是
fastapi dev pages.py的最后一条输出(上面是一整段 traceback),Server started at那行根本不会出现。 -
templates/的行为刚好相反:Jinja2Templates构造时不检查,缺目录、缺模板文件都能把服务起来,只是请求走到渲染那一步才 500。三种情况实测:1
2
3
4无 static/ 导入即 RuntimeError,服务起不来
有 static、无 templates/ 服务起来了,GET /page -> 500
有 templates、无 home.html 服务起来了,GET /page -> 500
jinja2.exceptions.TemplateNotFound: 'home.html' not found in search path所以「服务起不来」查静态目录,「页面 500 但服务起来了」查模板。
三种启动方式
-
官方其实给了三条路,差别在「谁帮你拼 uvicorn 参数」:
方式 命令 默认监听 reload 用途 uvicorn 直接起 python -m uvicorn main:app127.0.0.1:8000 要自己加 --reload通用,参数最全 fastapi devfastapi dev main.py127.0.0.1 默认开 开发 fastapi runfastapi run main.py0.0.0.0 默认关 生产(或前面再套一层) -
fastapi dev的官方说明是「等价于fastapi run,但开 reload 并只监听 127.0.0.1」,实测起来是这样:1
fastapi dev main.py --port 8000
1
2
3
4
5
6
7
8Starting FastAPI in development mode
Using import string: main:app
Server started at http://127.0.0.1:8000
Documentation at http://127.0.0.1:8000/docs
INFO: Will watch for changes in these directories: ['E:\\Desktop\\Test\\demo']
INFO: Started reloader process [46196] using WatchFiles
INFO: Started server process [41504]
INFO: Application startup complete.- 它会自动往
PYTHONPATH里加包所在目录,并自动找 app 对象:先按main.py/app.py/api.py(以及app/main.py这种)找文件,再在模块里找叫app或api的对象,找不到就用第一个FastAPI实例。所以变量名随便起的话,得用--app或--entrypoint指定。 - reload 靠
watchfiles(Rust 实现的文件监听),改文件即重启;生产环境不要开。
- 它会自动往
-
两个命令的选项差异,实测
--help:1
2fastapi dev 有 --reload-dir,没有 --workers
fastapi run 有 --workers、--root-path、--proxy-headers、--forwarded-allow-ips多进程部署要用
fastapi run --workers 4或uvicorn --workers 4,细节见 FastAPI-WebSocket、测试与部署。 -
⚠️ Windows 中文控制台上的一个坑:
fastapi dev --help会直接崩,因为它的帮助文本里有 emoji(🧪),而 rich 在 GBK 代码页下写不出来:1
UnicodeEncodeError: 'gbk' codec can't encode character '\U0001f9ea' in position 7
解法是强制 UTF-8,而不是去改终端代码页:
1
2set PYTHONUTF8=1
fastapi dev --help这条对
fastapi、uvicorn、alembic这些带 rich 输出的工具都适用,跟本机在 Django 那边遇到的 GBK 问题是同一类。
fastapi 命令报 Fatal error in launcher
-
用 conda 建的环境里跑
fastapi dev main.py,可能一条日志都没有就先崩:1
2
3Fatal error in launcher: Unable to create process using
'"C:\Users\task_178228053511861\croot\fastapi-split_1782280704753\_h_env\python.exe"
"D:\ProgramData\anaconda3\envs\fastapi\Scripts\fastapi.exe" dev main.py --port 8000': ???????????-
报错里那个
C:\Users\task_…\croot\…\_h_env\python.exe是 Anaconda 打包机上构建这个包时用的临时解释器路径,被烧死在Scripts\fastapi.exe这个启动器(launcher stub)里,本机当然没有这个文件。 -
后面那串
???????????不是乱码 bug,是 Windows 的「系统找不到指定的文件」被 GBK 控制台显示不出来。 -
触发条件很典型:pip 装过
fastapi[standard]之后又conda install fastapi,conda 会用自己那份 exe 覆盖 pip 生成的那份。判别方法:看文件时间是不是当天。1
Get-Item $env:CONDA_PREFIX\Scripts\fastapi.exe | Select-Object Name, LastWriteTime
1
2
3Name LastWriteTime
---- -------------
fastapi.exe 2026/6/24 14:00:00 # 打包时间,不是今天 -> 这份是 conda 塞进来的坏 stub
-
-
三种解法,本机都实测过:
-
绕过启动器(最快,不用装东西):
fastapi包自带__main__.py,所以模块方式一定能跑。1
python -m fastapi dev main.py --port 8000
1
2
3
4Starting FastAPI in development mode
Using import string: blocking_app:app
Server started at http://127.0.0.1:8994
Documentation at http://127.0.0.1:8994/docs(上面这段是本机实测输出,文件名与端口按当时探针的取值打印,命令换成自己的
main.py与端口即可。) -
重装,把 exe 写回正确路径:
1
pip install -U "fastapi[standard]"
1
2
3Would install fastapi-0.162.0 rich-toolkit-0.20.6
# 装完 fastapi.exe 的 LastWriteTime 变成当天,--version 正常输出
FastAPI CLI version: 0.0.32 -
只想修启动器、不动版本时:
pip install --force-reinstall --no-deps fastapi-cli。
-
-
混装的代价不止是启动器,conda 还会把版本拉回 defaults 通道的旧版。本机实测这次
conda install fastapi uvicorn之后:1
2
3fastapi 0.162.0 -> 0.138.0
uvicorn 0.54.0 -> 0.52.4
pydantic 2.14.3 -> 2.14.10.138 里连
telemetry这个构造参数都还没有(inspect.signature(FastAPI.__init__)实测查不到),所以「同一个环境里 conda 和 pip 混着装同一族包」要特别当心:要么全用 pip,要么全用 conda,别交叉覆盖。
async def 还是 def
-
官方规则只有一句话:处理器里要
await东西就用async def,全是同步库就写def,框架会把它丢进线程池。 听起来简单,写反的代价很大,所以本机实测了三种组合(单进程 uvicorn,每个请求耗时 0.4 秒,四个并发):写法 四个并发总耗时 期间夹一个 /fast的延迟async def+time.sleep(0.4)1.61s 1.21s def+time.sleep(0.4)0.42s 0.00s async def+await asyncio.sleep(0.4)0.41s 0.00s blocking_app.py 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24import asyncio
import time
from fastapi import FastAPI
app = FastAPI()
async def slow_async():
time.sleep(0.4) # 错:阻塞了整个事件循环
return {"kind": "async+time.sleep"}
def slow_sync():
time.sleep(0.4) # 对:def 版本被丢进线程池,事件循环照常跑
return {"kind": "def+time.sleep"}
async def slow_await():
await asyncio.sleep(0.4) # 对:让出事件循环
return {"kind": "async+await"} -
结论要说得比「异步更好」更准:
async def里出现阻塞调用,是四种组合里唯一会让所有请求排队的写法(1.61s ≈ 4×0.4s,连一个本来 0 毫秒的/fast都要等 1.21s)。反过来,写成def时同样的time.sleep完全不挡别人。 -
那
def是不是就安全了?不是,线程池有上限。实测这个上限:1
python -c "import anyio; anyio.run(lambda: print(anyio.to_thread.current_default_thread_limiter().total_tokens))"
1
total_tokens = 40
60 个各睡 0.4 秒的
def请求并发打过去,实测总耗时 0.82 秒——正好是两批(40 + 20),说明第 41 个开始就在排队了。 -
两种写法在哪个线程上跑,也能验:
thread_probe.py 1
2
3
4
5
6
7
8
9
10import threading
from fastapi import FastAPI
app = FastAPI()
def who():
return {"on_main_thread": threading.get_ident() == threading.main_thread()}1
GET /thread -> {'on_main_thread': False} # def 处理器确实在线程池里
顺带一个实用结论:
def处理器里可以放心用任何阻塞库(requests、mysqlclient、open()),因为它们本来就跑在线程里;但别在def处理器里访问contextvars之外的协程上下文,跨线程传不回去。 -
需要在
async def里调同步库时,别把函数改成def就完事,官方给的是显式挪进线程池:async_with_sync.py 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20import sqlite3
from fastapi import FastAPI
from fastapi.concurrency import run_in_threadpool
app = FastAPI()
async def rows():
data = await run_in_threadpool(_query) # 阻塞调用挪走,事件循环不卡
return {"count": len(data)}
def _query():
conn = sqlite3.connect("demo.sqlite")
try:
return conn.execute("select id from t").fetchall()
finally:
conn.close()实测
fastapi.concurrency导出的就是这几个工具,够用:1
2run_in_threadpool, iterate_in_threadpool, contextmanager_in_threadpool,
run_until_first_complete, CapacityLimiter流式响应里同步生成器要换成
iterate_in_threadpool(...),否则同样卡循环。 -
一句话记忆法:「写
async def就得对整条调用链负责」——里面只要有一个同步阻塞调用,前面所有请求都得等它。做不到就用def,或者显式run_in_threadpool。
工程骨架
-
单文件写到三十个接口就会失控,FastAPI 的拆法是
APIRouter。本机跑通的骨架:1
2
3
4
5
6
7
8
9demo/
main.py # 建 app、挂路由
core/
__init__.py
config.py # 配置
routers/
__init__.py
users.py
items.pyrouters/users.py 1
2
3
4
5
6
7
8
9
10
11
12
13from fastapi import APIRouter
router = APIRouter(prefix="/users", tags=["users"])
def list_users():
return {"users": ["lilei", "hanmeimei"]}
def get_user(user_id: int):
return {"user_id": user_id, "name": "lilei"}main.py 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15from fastapi import FastAPI
from core.config import Settings
from routers import items, users
settings = Settings()
app = FastAPI(title=settings.app_name)
app.include_router(users.router)
app.include_router(items.router)
def health():
return {"ok": True, "name": settings.app_name} -
实测请求结果:
1
2
3
4
5
6/health -> 200 {"ok": true, "name": "shop-api"}
/users -> 200 {"users": ["lilei", "hanmeimei"]}
/users/3 -> 200 {"user_id": 3, "name": "lilei"}
/items -> 200 {"items": [1, 2]}
/users/abc -> 422 {"detail": [{"type": "int_parsing", "loc": ["path", "user_id"], ...}]}
/nope -> 404 {"detail": "Not Found"}-
prefix="/users"+@router.get("")得到/users;写成@router.get("/")得到/users/。两种写法都能访问通,靠的是redirect_slashes(默认开)在中间补 307:1
2
3
4
5GET /u1 -> 307 Location: /u1/ # 路由写成 @router.get("/") 时
GET /u1/ -> 200
GET /u2 -> 200 # 路由写成 @router.get("") 时
GET /u2/ -> 307 Location: /u2
openapi paths: ['/u1/', '/u2'] # 文档里是两个不同的路径所以统一用空字符串:少一次 307 往返,前端按文档里的路径请求也正好命中,不会每次都先吃一个重定向。
-
/users/abc的 422 不用你写任何校验代码:路径参数声明了int,转换失败自动返回结构化的校验错误。
-
-
⚠️ 一个 0.162 的行为变化,写脚本时会撞上:
include_router现在是懒加载的,app.routes里会出现没有path属性的_IncludedRouter对象。老写法直接崩:1
python -c "import main; print([r.path for r in main.app.routes])"
1
AttributeError: '_IncludedRouter' object has no attribute 'path'
实测的路由分布是这样:
1
2
3app.routes 总数 7
类型分布: Counter({'Route': 4, '_IncludedRouter': 2, 'APIRoute': 1})
直接能看到的 path: ['/openapi.json', '/docs', '/docs/oauth2-redirect', '/redoc', '/health']也就是说
/users、/items这些挂在_IncludedRouter里,不在app.routes的第一层。要拿到全部路径,用受支持的公开结果,别去翻内部结构:list_routes.py 1
2
3
4from main import app
paths = sorted(app.openapi()["paths"])
print(paths)1
['/health', '/items', '/users', '/users/{user_id}']
- 单个 URL 的生成仍然正常:
app.url_path_for("get_user", user_id=3)实测返回/users/3(url_path_for已经会钻进被包含的路由里找)。 - 需要遍历「有效路由」时,
_IncludedRouter暴露了effective_candidates(),实测能拿到['/users', '/users/{user_id}', '/items'],但这是内部实现,别写进生产代码。
- 单个 URL 的生成仍然正常:
-
include_router还能带公共依赖和前缀,这一层是「整组接口都要登录」这类需求的正确落点,写法见 FastAPI-依赖注入与异常中间件。
配置从哪来
-
官方推荐用
pydantic-settings(fastapi[standard]已经带了),好处是配置项也有类型校验:core/config.py 1
2
3
4
5
6
7
8
9
10
11
12
13from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8",
extra="ignore")
app_name: str = "shop-api"
max_limit: int = 10
secret: str = "change-me"
settings = Settings() -
优先级实测(环境变量 >
.env文件 > 代码默认值):1
2
3只走环境变量: {'app_name': 'from-env', 'max_limit': 50, 'secret': 'none'}
有 .env 时(环境变量优先): {'app_name': 'from-env', 'max_limit': 50, 'secret': 'sk-123'}
去掉环境变量后 .env 生效: {'app_name': 'from-dotenv','max_limit': 50, 'secret': 'sk-123'}- 字段名
app_name会自动匹配大写的环境变量APP_NAME,不需要声明。 .env里有代码里没声明的键时,不加extra="ignore"会直接ValidationError,实测报Extra inputs are not permitted。- 环境变量里放了非法值也不会静默兜底:
MAX_LIMIT=abc实测抛ValidationError: 1 validation error for S,服务起不来——这是好事,配置写错应该在启动时就炸。
- 字段名
-
⚠️ 老教程里的
class Config写法在 v2 已经不适用,实测既告警又报错:legacy_settings.py 1
2
3
4
5
6
7
8
9# 老写法(Pydantic v1),v2 下会报 PydanticDeprecatedSince20,且 env_file 不生效
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
app_name: str = "default"
class Config:
env_file = ".env"1
2PydanticDeprecatedSince20: Support for class-based `config` is deprecated, use ConfigDict instead.
ValidationError: 2 validation errors for Settings正确写法只有
model_config = SettingsConfigDict(...)这一条。 -
密钥类配置(
password、JWT 签名密钥、数据库口令)走环境变量或.env,并且把.env加进.gitignore,不要把明文写进代码仓库。 -
配置对象怎么注入到处理器里(
Depends(Settings())的写法与坑)见 FastAPI-依赖注入与异常中间件。
文档三件套
-
服务起来后有三个内置路径,实测状态码:
1
2
3
4/docs -> 200 Swagger UI
/redoc -> 200 ReDoc
/openapi.json -> 200 OpenAPI 描述
/docs/oauth2-redirect -> 200 OAuth2 回调页(做认证时才用得到)这份文档不是「示例代码生成的假文档」,它就是路由与模型的真实描述,所以接口一改文档就跟着改,这也是 FastAPI 最大的日常收益。
-
生成的描述用的是 OpenAPI 3.1.0,默认标题和版本是这样:
1
2openapi: 3.1.0
info: {'title': 'FastAPI', 'version': '0.1.0'}title和version在FastAPI(title=..., version=...)里改,别在路由上零散改。 -
想关掉或改路径,实测三种情况:
docs_switch.py 1
2
3
4from fastapi import FastAPI
off = FastAPI(docs_url=None, redoc_url=None, openapi_url=None)
moved = FastAPI(docs_url="/api-docs", openapi_url="/my-openapi.json")1
2全关:/docs 404 | /openapi.json 404
改路径:/api-docs 200 | /my-openapi.json 200 | 原来的 /docs 404- 只关
/docs不关openapi_url是没用的——描述文件还在全网可读,等于没藏。要关就三个一起关。 - 生产环境是否公开文档没有标准答案:内部系统一般留着方便联调,公网接口通常关掉或加鉴权(用依赖包一层,见 FastAPI-安全与认证)。
- 只关
默认错误响应
-
不用写任何代码,FastAPI 的默认错误就是 JSON,实测:
1
2
3
4GET /nope -> 404 {"detail":"Not Found"}
POST /only-get -> 405 {"detail":"Method Not Allowed"}
GET /users/abc -> 422 {"detail":[{"type":"int_parsing","loc":["path","user_id"],
"msg":"Input should be a valid integer, ..."}]}- 404/405 由 Starlette 的路由层给,422 由校验层给,三者结构不同:422 的
detail是列表,每项带type、loc、msg,前端可以直接按loc定位到具体输入框。 - 校验错误的完整结构、字段名含义和自定义方式在 FastAPI-请求体与Pydantic校验里逐条讲。
- 404/405 由 Starlette 的路由层给,422 由校验层给,三者结构不同:422 的
-
抛业务错误用
HTTPException,它和默认错误走同一个detail结构:raise_error.py 1
2
3
4
5
6
7
8
9
10
11from fastapi import FastAPI, HTTPException, status
app = FastAPI()
def get_goods(goods_id: int):
if goods_id != 1:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail="商品不存在")
return {"goods_id": 1, "name": "键盘"}1
2/goods/1 -> 200 {"goods_id": 1, "name": "键盘"}
/goods/9 -> 404 {"detail":"商品不存在"}- 必须
raise,不能return HTTPException(...);return一个Response对象才是合法的(见 FastAPI-响应模型与OpenAPI文档)。 - 中文
detail能正常输出,是因为 FastAPI 序列化时不转义非 ASCII;换成ensure_ascii=True的自定义响应就会变成\uXXXX。
- 必须