首页
/ FastAPI 配置管理实战:用 Pydantic Settings 与环境变量打造安全、可测试的应用配置

FastAPI 配置管理实战:用 Pydantic Settings 与环境变量打造安全、可测试的应用配置

2026-09-07 20:28:51作者:平淮齐Percy

导读

在真实生产环境中,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_EMAILAPP_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,
    }

关键点在于:

  1. 定义 get_settings_override(),在创建新的 Settings 对象时为 admin_email 传入新的值,并返回该新对象;
  2. 通过 app.dependency_overrides[get_settings] = get_settings_override 注册覆盖;
  3. 之后测试断言 /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 版本做个说明:仓库为每个示例都提供了 py310an_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.pymain.py test_app01.py
依赖注入 + 测试覆盖 app02_an_py310/ test_app02.py
.env + @lru_cache 最终形态 app03_an_py310/config.pymain.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 应用在生产环境中管理密钥、数据库凭证等敏感且易变配置的标准姿势。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391