在项目从本地开发走向线上部署的过程中,「配置」往往是第一个让人头疼的问题:数据库连接串、Redis 地址、JWT 密钥、第三方 API Key……如果把这些值硬编码在代码里,每次切换环境都要改代码,既不安全也容易出错。FastAPI 本身不强制配置方案,但官方推荐使用 pydantic-settings(Pydantic v2 时代的 BaseSettings)来统一管理。本文系统讲解如何在 FastAPI 项目中用 pydantic-settings 构建清晰、可测试、环境隔离的配置体系。

为什么选择 pydantic-settings

传统做法是用 os.getenv("KEY") 散落在各处读取环境变量,存在几个明显问题:

  1. 类型缺失os.getenv 返回的永远是 strNone,端口号、布尔开关需要手动转换,稍不注意就报错。
  2. 校验滞后:缺少某个关键变量时,往往要等到运行到对应代码才发现,启动阶段没有任何提示。
  3. 难以测试:测试用例里要反复 os.environ["KEY"] = ... 设置和清理,容易污染全局状态。

pydantic-settings 借助 Pydantic 的类型系统,在应用启动时就完成类型转换、默认值填充和必填校验,缺了关键配置直接启动失败,把问题暴露在最早阶段。

安装与最简示例

pydantic-settings 是独立包,需要单独安装:

1
pip install pydantic-settings

定义一个继承 BaseSettings 的配置类,把每个配置项声明为带类型注解的字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
# 基本配置
app_name: str = "My FastAPI App"
debug: bool = False

# 数据库
database_url: str

# Redis
redis_url: str = "redis://localhost:6379/0"

# JWT
jwt_secret: str
jwt_algorithm: str = "HS256"
jwt_expire_minutes: int = 60

model_config = SettingsConfigDict(
env_file=".env", # 自动读取 .env 文件
env_file_encoding="utf-8",
case_sensitive=False, # 环境变量名大小写不敏感
)


settings = Settings()

对应的 .env 文件:

1
2
3
4
DEBUG=true
DATABASE_URL=postgresql+asyncpg://user:pass@localhost:5432/mydb
JWT_SECRET=super-secret-key-change-in-production
JWT_EXPIRE_MINUTES=120

Settings() 实例化时会按以下优先级读取:环境变量 > .env 文件 > 类中默认值DEBUG 会被自动转换为布尔值,JWT_EXPIRE_MINUTES 会被转为 int,类型转换由 Pydantic 自动完成。

在 FastAPI 中使用配置

方式一:模块级单例(最简单)

config.py 中实例化后,直接在各处导入使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
# app/config.py
from pydantic_settings import BaseSettings


class Settings(BaseSettings):
database_url: str
jwt_secret: str
debug: bool = False

model_config = SettingsConfigDict(env_file=".env")


settings = Settings()
1
2
3
4
5
6
7
8
9
10
# app/main.py
from fastapi import FastAPI
from .config import settings

app = FastAPI(title=settings.app_name, debug=settings.debug)


@app.get("/info")
async def info():
return {"app": settings.app_name, "debug": settings.debug}

这种方式适合中小型项目,缺点是配置在测试时不好替换(需要 mock 或改环境变量)。

方式二:通过依赖注入(推荐)

更灵活的做法是用 lru_cache 把配置作为依赖注入,便于测试时覆盖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from functools import lru_cache
from fastapi import Depends, FastAPI
from pydantic_settings import BaseSettings


class Settings(BaseSettings):
app_name: str = "My API"
database_url: str
jwt_secret: str

model_config = SettingsConfigDict(env_file=".env")


@lru_cache
def get_settings() -> Settings:
"""用 lru_cache 缓存,整个应用生命周期内只实例化一次"""
return Settings()


app = FastAPI()


@app.get("/info")
async def info(settings: Settings = Depends(get_settings)):
return {"app": settings.app_name}

lru_cache 保证 Settings() 只在第一次请求时构造一次,后续直接返回缓存。测试时可以调用 get_settings.cache_clear() 重置,或用 app.dependency_overrides 覆盖:

1
2
3
4
5
6
7
8
9
10
from fastapi.testclient import TestClient
from .config import Settings, get_settings


def get_test_settings():
return Settings(database_url="sqlite:///:memory:", jwt_secret="test-secret")


app.dependency_overrides[get_settings] = get_test_settings
client = TestClient(app)

这样测试用例完全不依赖真实环境变量,干净且可控。

嵌套配置

当配置项很多时,把它们拍平在一个类里会很难维护。pydantic-settings 支持嵌套模型,配合环境变量前缀可以让结构更清晰:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from pydantic import BaseModel
from pydantic_settings import BaseSettings, SettingsConfigDict


class DatabaseSettings(BaseModel):
url: str
pool_size: int = 10
max_overflow: int = 20
echo: bool = False


class RedisSettings(BaseModel):
url: str = "redis://localhost:6379/0"
max_connections: int = 50


class Settings(BaseSettings):
database: DatabaseSettings
redis: RedisSettings = RedisSettings()
jwt_secret: str

model_config = SettingsConfigDict(
env_file=".env",
env_nested_delimiter="__", # 用双下划线表示嵌套
)

对应的 .env

1
2
3
4
DATABASE__URL=postgresql://user:pass@localhost:5432/mydb
DATABASE__POOL_SIZE=20
DATABASE__ECHO=true
JWT_SECRET=my-secret

env_nested_delimiter="__" 告诉 pydantic-settings 把 DATABASE__URL 解析为 database.url。这样不同模块的配置互不干扰,结构一目了然。

多环境切换

实际项目通常有 dev / test / prod 多套环境。常见做法是用多个 .env 文件,通过 ENV 环境变量决定加载哪一个:

1
2
3
4
5
6
7
8
9
10
11
12
13
import os
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
app_name: str
database_url: str
debug: bool = False

model_config = SettingsConfigDict(
env_file=(f".env.{os.getenv('ENV', 'dev')}", ".env"),
env_file_encoding="utf-8",
)

env_file 接受一个元组,后面的文件优先级更低,作为兜底默认值。这样你可以:

  • .env.dev 放开发环境的配置(debug=true、本地数据库)
  • .env.prod 放生产配置(debug=false、线上数据库)
  • .env 放所有环境共享的默认值

启动时通过 ENV=prod uvicorn app.main:app 切换。注意 .env.prod 这类文件不应提交到 Git,应在 .gitignore 中排除,敏感信息只存在于服务器环境变量中。

校验与默认值进阶

必填校验

没有默认值的字段是必填的。如果启动时没有提供,实例化会直接抛出 ValidationError

1
2
3
4
5
6
7
class Settings(BaseSettings):
jwt_secret: str # 无默认值,必须提供


# 若未设置 JWT_SECRET,启动即报错:
# pydantic_core.ValidationError: 1 validation error for Settings
# jwt_secret - Field required

这种「快速失败」行为非常有价值——避免应用带着不完整配置跑到线上。

枚举与约束

可以结合 Pydantic 的 FieldEnum 进一步约束取值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from enum import Enum
from pydantic import Field
from pydantic_settings import BaseSettings


class Env(str, Enum):
dev = "dev"
test = "test"
prod = "prod"


class Settings(BaseSettings):
env: Env = Env.dev
log_level: str = Field("INFO", pattern="^(DEBUG|INFO|WARNING|ERROR)$")
port: int = Field(8000, ge=1, le=65535)

传入非法值时启动就会失败,等于在配置层就建立了一道校验防线。

敏感信息保护

1. 永远不要把密钥写进代码

.env 文件应加入 .gitignore,绝不提交。可以提交一个 .env.example 作为模板:

1
2
3
4
# .env.example —— 提交到仓库作为模板
DEBUG=false
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
JWT_SECRET=change-me-in-production

2. 使用 SecretStr 防止日志泄漏

对于 JWT 密钥、数据库密码等,可以用 SecretStr 类型,避免在日志或调试输出中明文打印:

1
2
3
4
5
6
7
8
9
10
11
12
13
from pydantic import SecretStr
from pydantic_settings import BaseSettings


class Settings(BaseSettings):
jwt_secret: SecretStr
db_password: SecretStr


settings = Settings(jwt_secret="my-secret", db_password="p@ssw0rd")

print(settings.jwt_secret) # **********
print(settings.jwt_secret.get_secret_value()) # my-secret —— 需要显式获取

reprstr 输出的都是 **********,只有显式调用 get_secret_value() 才能拿到真实值,能有效防止日志中误打印密钥。

3. 生产环境用真正的环境变量

在 Docker、Kubernetes 或云平台上,不要依赖 .env 文件,而是直接通过平台机制注入环境变量。pydantic-settings 会自动读取进程环境变量,无需额外配置:

1
2
3
# Dockerfile
ENV DATABASE_URL=postgresql://prod-db:5432/app
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0"]

或在 docker run 时传入:

1
docker run -e JWT_SECRET=$JWT_SECRET -e DATABASE_URL=$DATABASE_URL my-app

小结

pydantic-settings 让 FastAPI 的配置管理变得类型安全、可测试、环境隔离:

  1. 继承 BaseSettings 声明配置项,自动完成类型转换与校验。
  2. model_config 控制 .env 文件加载、嵌套分隔符等行为。
  3. 依赖注入 + lru_cache 是推荐的集成方式,便于单元测试时覆盖。
  4. 嵌套模型 + 前缀 适合大型项目,让配置结构清晰可维护。
  5. .env 文件 + ENV 变量 实现开发/测试/生产环境隔离。
  6. SecretStr 保护敏感信息,避免日志泄漏。
  7. 生产环境直接用平台环境变量.env 仅用于本地开发。

掌握这套配置体系后,你可以从容应对从本地开发到容器化部署的全流程,让配置不再成为项目的隐患。