FastAPI 配置管理实战:用 Pydantic Settings 与环境变量打造安全、可测试的应用配置
导读
在真实生产环境中,FastAPI 应用几乎总要依赖外部配置:数据库连接串、邮件服务凭证、密钥等,这些值既会随环境变化、又往往敏感。本篇文章围绕 FastAPI 官方文档 "Settings and Environment Variables" 的主题,系统讲解如何用 Pydantic Settings 从环境变量和 .env 文件中读取配置、完成类型转换与校验,并通过依赖注入与 @lru_cache 缓存机制,写出配置集中、易于测试、每次请求零重复读取的应用。读完本文,你将掌握一套可直接落地的 FastAPI 配置管理实战方案。
为什么需要"外部设置"?
很多场景下,你的应用需要一些外部设置(settings)或配置(configurations),例如:
- 密钥(secret keys)
- 数据库凭证(database credentials)
- 邮件服务的凭证(credentials for email services)
这些设置大多是可变的(例如 database URLs),而另一些则是敏感的(例如 secrets)。因此,业界普遍的做法是把它们放进 环境变量(environment variables) 中,由应用在运行时读取,而不是硬编码进源码。
所谓环境变量(简称 env var),是指存在于 Python 代码之外、操作系统层面的一种值,它既可以被你的应用读取,也能被其他程序读取。当你运行某条命令时,可以临时为该命令创建环境变量(下文会给出 Linux、macOS、Windows 各自的具体写法)。
环境变量的类型与校验约束
环境变量只能承载文本字符串,因为它们位于 Python 之外,必须与其他程序乃至整个系统兼容(还要跨 Linux、Windows、macOS 等不同操作系统)。
这意味着:从环境变量里读出的任何值,在 Python 中都是 str;任何类型转换或校验都必须在代码中完成。
用 Pydantic 的 Settings 统一处理
幸运的是,Pydantic 提供了优秀的工具来统一处理来自环境变量的配置,官方称之为 Pydantic: Settings management。结合 FastAPI 使用时,它几乎零成本地与类型标注(type annotations)和校验体系天然衔接。
安装 pydantic-settings
将 pydantic-settings 包加入你的项目:
$ uv add pydantic-settings
---> 100%
如果你安装的是 all 扩展集,它也会一并被包含:
$ uv add "fastapi[all]"
---> 100%
创建 Settings 对象
从 Pydantic 中导入 BaseSettings,然后像定义 Pydantic 模型一样创建它的子类:用类型标注声明类属性(class attributes),并给出可选的默认值。
你可以使用所有在 Pydantic 模型上常用的校验特性与工具,比如各种数据类型,以及通过 Field() 追加额外校验。
下面的完整示例来自仓库源码 docs_src/settings/tutorial001_py310.py:
from fastapi import FastAPI
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
app_name: str = "Awesome API"
admin_email: str
items_per_user: int = 50
settings = Settings()
app = FastAPI()
@app.get("/info")
async def info():
return {
"app_name": settings.app_name,
"admin_email": settings.admin_email,
"items_per_user": settings.items_per_user,
}
提示:如果你需要一份可以快速复制粘贴的代码,不要使用上面这个"单文件全局实例"示例,请使用本文后续的"结合依赖注入 +
@lru_cache+.env"的最终示例。
当你实例化 Settings 类(此处是 settings 对象)时,Pydantic 会以大小写不敏感的方式读取环境变量:也就是说,全大写的变量 APP_NAME 仍然能被读取并映射到属性 app_name。
随后 Pydantic 会做数据转换与校验。因此当你使用该 settings 对象时,拿到的数据必然是你声明的类型(例如 items_per_user 会是 int)。
小结:哪怕从环境变量读进来的是字符串,一旦声明了
items_per_user: int,Pydantic 就会自动完成"50" → 50的转换与类型校验,无需手写int(os.getenv(...))。
在应用中"使用"这些 settings
在上面的代码里,/info 路径操作直接访问 settings 对象的属性并返回:
@app.get("/info")
async def info():
return {
"app_name": settings.app_name,
"admin_email": settings.admin_email,
"items_per_user": settings.items_per_user,
}
运行服务器:把配置作为环境变量传入
接下来,以环境变量的形式传入配置并启动服务器。例如,设置 ADMIN_EMAIL 和 APP_NAME:
Linux、macOS、Windows Bash
$ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp" uv run fastapi run main.py
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
Windows PowerShell
$ $Env:ADMIN_EMAIL = "deadpool@example.com"
$ $Env:APP_NAME = "ChimichangApp"
$ uv run fastapi run main.py
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
提示:在 Bash 中,若想为单条命令设置多个环境变量,请用空格把它们分隔开,并全部放在命令之前(如上面的写法)。PowerShell 则用两条
$Env:...赋值语句完成。
运行后:
admin_email设置会被设为"deadpool@example.com";app_name会是"ChimichangApp";items_per_user因为没有对应的环境变量,保持默认值50。
把 Settings 放进独立的模块文件
你可以把这些 settings 放到另一个模块文件中——正如官方教程 Bigger Applications - Multiple Files 介绍的那样。目录结构需要包含 __init__.py(让目录成为一个 Python 包),参见仓库中的 docs_src/settings/app01_py310/。
例如,创建一个 config.py 文件(完整代码见 config.py):
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
app_name: str = "Awesome API"
admin_email: str
items_per_user: int = 50
settings = Settings()
然后在 main.py 中使用它(完整代码见 main.py):
from fastapi import FastAPI
from .config import settings
app = FastAPI()
@app.get("/info")
async def info():
return {
"app_name": settings.app_name,
"admin_email": settings.admin_email,
"items_per_user": settings.items_per_user,
}
这样,Settings 的定义与读取逻辑被收敛到 config.py,业务代码(main.py)只需要 from .config import settings 即可。
把 Settings 放进依赖注入
有时候,用**依赖(dependency)**来提供 settings 会比"维护一个到处 import 的全局对象 settings"更有优势。
这在测试场景下尤其有用:因为依赖可以通过 app.dependency_overrides 非常容易地用自定义 settings 覆盖。
配置文件(不含默认实例)
延续上一个示例,你的 config.py 可以改成这样(完整代码见 docs_src/settings/app02_an_py310/config.py):
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
app_name: str = "Awesome API"
admin_email: str
items_per_user: int = 50
注意:这里不再创建默认实例 settings = Settings(),只保留类的定义。
主应用文件(暴露一个工厂依赖)
现在我们创建一个依赖函数,每次被调用时返回一个新的 config.Settings():
from functools import lru_cache
from typing import Annotated
from fastapi import Depends, FastAPI
from .config import Settings
app = FastAPI()
@lru_cache
def get_settings():
return Settings()
@app.get("/info")
async def info(settings: Annotated[Settings, Depends(get_settings)]):
return {
"app_name": settings.app_name,
"admin_email": settings.admin_email,
"items_per_user": settings.items_per_user,
}
提示:上面的
@lru_cache我们稍后详谈;目前你可以先把get_settings()当作一个普通函数来理解。
注意 info 参数使用了 Annotated[Settings, Depends(get_settings)]:FastAPI 会解析依赖并把结果注入 settings 参数,路径操作函数就可以在需要的地方直接使用它。
Settings 与测试:通过依赖覆盖注入自定义配置
上述做法的最大收益是测试非常简单。仓库中的测试示例 docs_src/settings/app02_an_py310/test_main.py 展示了完整流程:
from fastapi.testclient import TestClient
from .config import Settings
from .main import app, get_settings
client = TestClient(app)
def get_settings_override():
return Settings(admin_email="testing_admin@example.com")
app.dependency_overrides[get_settings] = get_settings_override
def test_app():
response = client.get("/info")
data = response.json()
assert data == {
"app_name": "Awesome API",
"admin_email": "testing_admin@example.com",
"items_per_user": 50,
}
关键点在于:
- 定义
get_settings_override(),在创建新的Settings对象时为admin_email传入新的值,并返回该新对象; - 通过
app.dependency_overrides[get_settings] = get_settings_override注册覆盖; - 之后测试断言
/info返回的数据确实使用了被覆盖的admin_email。
仓库的自动化测试 tests/test_tutorial/test_settings/test_app02.py 对这一行为做了双向验证:既有 monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com") 后断言从真实环境变量读取成功,也有直接调用 test_main_mod.test_app() 验证依赖覆盖生效。
从 .env 文件读取设置
当配置项很多、且在不同环境中变化频繁时,把它们集中放到一个文件里,再像读取环境变量一样读取,会方便很多。
这种实践太普遍,以至于它有专门的名字:这些"环境变量"通常放在一个 .env 文件里,该文件被称为 dotenv 文件。
提示:以点(
.)开头的文件在 Unix-like 系统(如 Linux 和 macOS)中是隐藏文件。但 dotenv 文件其实并不强制使用这个精确文件名。
Pydantic 借助外部库来支持读取这类文件(详见 Pydantic Settings: Dotenv (.env) support)。要让它工作,需要先把 python-dotenv 加入项目:
$ uv add python-dotenv
准备 .env 文件
你可以在项目根目录放这样一个 .env 文件:
ADMIN_EMAIL="deadpool@example.com"
APP_NAME="ChimichangApp"
从 .env 读取 settings:SettingsConfigDict(env_file=".env")
然后更新你的 config.py(完整代码见 docs_src/settings/app03_an_py310/config.py):
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
app_name: str = "Awesome API"
admin_email: str
items_per_user: int = 50
model_config = SettingsConfigDict(env_file=".env")
这里我们在 Pydantic Settings 类内部通过 model_config 配置了 env_file,把它的值设为我们要使用的 dotenv 文件名。也就是说:
- 只要
model_config = SettingsConfigDict(env_file=".env")声明到位,Pydantic 启动时就会解析项目中的.env文件; - 声明的字段会先从
.env/ 环境变量中取有值; model_config属性仅用于 Pydantic 自身的配置,可进一步参考 Pydantic: Concepts: Configuration。
用 @lru_cache 只创建一次 Settings
从磁盘读取文件通常是代价较高(较慢)的操作,因此我们通常希望只读一次,然后在后续请求中复用同一个 settings 对象,而不是每个请求都重读。
但每次执行 Settings(),都会创建一个全新的对象,并在创建时重新读取 .env 文件:
def get_settings():
return Settings()
如果依赖函数只是上面这样,那么每个请求都会创建对象、每个请求都会读一遍 .env 文件。⚠️
而一旦在依赖函数上加了 @lru_cache 装饰器,Settings 对象就只会被创建一次——第一次被调用时创建。✔️ 具体示例见 docs_src/settings/app03_an_py310/main.py:
from functools import lru_cache
from typing import Annotated
from fastapi import Depends, FastAPI
from . import config
app = FastAPI()
@lru_cache
def get_settings():
return config.Settings()
@app.get("/info")
async def info(settings: Annotated[config.Settings, Depends(get_settings)]):
return {
"app_name": settings.app_name,
"admin_email": settings.admin_email,
"items_per_user": settings.items_per_user,
}
此后,后续请求对依赖 get_settings() 的每一次调用,都不会再执行 get_settings() 内部代码、也不会新建 Settings 对象,而是直接返回第一次调用时创建的那个对象——一次又一次地复用。
这里对
Annotated版本做个说明:仓库为每个示例都提供了py310与an_py310(即使用Annotated显式标注)两套写法,如 docs_src/settings/app02_py310/ 与 docs_src/settings/app02_an_py310/。推荐使用Annotated[Settings, Depends(get_settings)]这种显式依赖写法,它避免了对 FastAPI 默认参数判断机制的依赖,语义也更清晰。两种写法的行为在仓库测试 tests/test_tutorial/test_settings/test_app02.py 中都被参数化覆盖验证。
@lru_cache 技术细节
@lru_cache 会修改被它装饰的函数:函数首次被调用时返回的那个值,会被缓存下来;后续调用不再重复执行函数内部代码重新计算,而是直接返回首次的结果。
因此:该函数会为每一种参数组合各执行一次;之后,每当函数以完全相同的参数组合被调用,都会直接复用该组合首次返回的结果。例如:
@lru_cache
def say_hi(name: str, salutation: str = "Ms."):
return f"Hello {salutation} {name}"
你的程序执行流程可能如下(绿色块 = 真正执行函数代码;青色块 = 直接返回已缓存结果):
sequenceDiagram
participant code as Code
participant function as say_hi()
participant execute as Execute function
rect rgba(0, 255, 0, .1)
code ->> function: say_hi(name="Camila")
function ->> execute: execute function code
execute ->> code: return the result
end
rect rgba(0, 255, 255, .1)
code ->> function: say_hi(name="Camila")
function ->> code: return stored result
end
rect rgba(0, 255, 0, .1)
code ->> function: say_hi(name="Rick")
function ->> execute: execute function code
execute ->> code: return the result
end
rect rgba(0, 255, 0, .1)
code ->> function: say_hi(name="Rick", salutation="Mr.")
function ->> execute: execute function code
execute ->> code: return the result
end
rect rgba(0, 255, 255, .1)
code ->> function: say_hi(name="Rick")
function ->> code: return stored result
end
rect rgba(0, 255, 255, .1)
code ->> function: say_hi(name="Camila")
function ->> code: return stored result
end
在我们的依赖 get_settings() 场景中,函数甚至不接收任何参数,因此它永远返回同一个值。
这样一来,它几乎就像一个全局变量——因为它始终复用同一个对象。但它底层是一条依赖函数,所以测试时仍然可以非常容易地覆盖它(结合上文 app.dependency_overrides)。这恰好同时拿到了"全局单例"与"可测试性"两个好处。
@lru_cache 属于 functools 模块,而 functools 是 Python 标准库的一部分(详见 Python docs for @lru_cache)。
仓库中的示例与测试对照
如果你想进一步对照真实代码深入阅读,本文所有代码示例都能在当前仓库中找到对应文件:
| 阶段 | 示例源码 | 对应自动化测试 |
|---|---|---|
| 单文件直连环境变量 | tutorial001_py310.py | test_tutorial001.py |
| 独立模块存放 Settings | app01_py310/config.py、main.py | test_app01.py |
| 依赖注入 + 测试覆盖 | app02_an_py310/ | test_app02.py |
.env + @lru_cache 最终形态 |
app03_an_py310/config.py、main.py | test_app03.py |
值得留意的是,仓库的自动化测试在导入示例模块前会先通过 monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com") 设置真实的环境变量(见 test_tutorial001.py 第 10 行),从而证实了"环境变量大小写不敏感、类型自动转换、默认值兜底"这三条核心行为是端到端生效的。
小结(Recap)
利用 Pydantic Settings,你可以以 Pydantic 模型的全部能力来处理应用的 settings 或配置:
- 类型标注 + 默认值:像定义 Pydantic 模型一样声明配置字段;
- 环境变量/
.env自动读取:BaseSettings大小写不敏感地读取环境变量,SettingsConfigDict(env_file=".env")支持 dotenv 文件; - 借助依赖注入简化测试:把
get_settings()作为依赖暴露,测试时用app.dependency_overrides覆盖即可注入不同配置; @lru_cache避免重复读盘:只在首次调用时创建Settings并读取 dotenv 文件,后续请求全部复用同一个对象,同时保留测试覆盖能力。
这套"环境变量 + Pydantic Settings + 依赖注入 + 缓存"的组合,就是 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
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00