Skip to content

第 12 章 Settings 应用配置 ​

前面几章用 Pydantic 处理文章、作者和评论等业务数据。程序本身也有一组运行配置,例如:

text
APP_NAME=Article App
ENV=dev
DEBUG=true
PORT=8000
DATA_DIR=./data

如果把这些值散落着写进代码,后面会很难维护:

  • 配置散在各处,不方便一起管理
  • 开发环境和生产环境可能需要不同值
  • 读到的值通常是字符串,还得手动转换类型
  • 配置写错时,希望程序一启动就发现

本章用 pydantic-settings 从 .env 或环境变量读取这些值,再转换并校验成类型明确的配置对象。

先走一遍轻量主线:

  • BaseSettings
  • .env
  • SettingsConfigDict
  • 默认值、必填项、类型转换、报错

本章学习目标 ​

应用配置为什么要单独管理?从这个问题出发,你会用 BaseSettings 定义配置模型,通过 SettingsConfigDict(env_file=".env") 读取文件,再观察同名环境变量覆盖 .env。配置读取时的类型转换、校验,以及默认值和必填项的区别,也要能看明白。

Settings 基础模型 ​

还是用“文章应用”做例子。这次不处理文章内容,而是看程序启动时需要的配置:

text
app_name  -> 应用名称
env       -> 运行环境
debug     -> 是否开启调试
port      -> 运行端口
data_dir  -> 数据目录

本地演示时,可以把它们放进 .env:

txt
APP_NAME=Article App
ENV=dev
DEBUG=true
PORT=8000
DATA_DIR=./data

启动程序后,直接创建配置对象:

python
settings = Settings()

print(settings.app_name)
print(settings.debug)
print(settings.port)

读取后,字段应当已经是对应的类型:

settings.debug 是 bool,settings.port 是 int。

可以先看整体流程:

BaseSettings 多源配置装载与覆盖优先级瀑布

每个示例都可以复制到 .code/main.py 运行,下一步再用新代码覆盖它。涉及 .env 的练习,还要把对应内容保存到同目录的 .env 文件。

按课程首页创建好本地 .code 练习目录后,pydantic-settings 就已安装。可以在 .code 目录确认版本:

bash
uv run python -c "import pydantic_settings; print(pydantic_settings.__version__)"

先跑最小的配置模型。把下面代码保存为 .code/main.py:

py
from pydantic_settings import BaseSettings


class Settings(BaseSettings):
    app_name: str = "Article App"
    debug: bool = False
    port: int = 8000


settings = Settings()

print(settings.app_name)
print(settings.debug)
print(settings.port)

运行:

bash
uv run python main.py

如果当前环境没有同名变量覆盖这些默认值,输出会是:

text
Article App
False
8000

后面的步骤会继续拆开配置类和读取过程;先运行完整例子,再对照各段代码。

应用配置

应用配置是程序运行时需要的一组参数,常见的有:

  • 应用名称
  • 运行环境
  • 调试开关
  • 端口
  • 路径

文章和评论描述程序要处理什么,配置则描述程序怎么运行。

BaseSettings

BaseSettings 是 pydantic-settings 提供的配置模型基类,定义方式和 BaseModel 很像:

例如:

python
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    app_name: str = "Article App"
    debug: bool = False
    port: int = 8000

主要区别在用途:

text
BaseModel    -> 更偏业务数据建模
BaseSettings -> 更偏应用配置管理

业务模型与配置模型

BaseModel 业务模型 vs BaseSettings 配置模型对比架构

Settings 读取流程

BaseSettings 多源配置装载与覆盖优先级瀑布

从这个例子里先认清几件事:

Settings 模型

python
class Settings(BaseSettings):

继承 BaseSettings,说明这个类用来管理配置。

配置项类型

python
app_name: str
debug: bool
port: int

默认值

python
debug: bool = False
port: int = 8000

创建配置对象

python
settings = Settings()

调用 Settings() 时会读取配置,并按字段定义进行校验。

最小 Settings 流程

应用启动阶段 Settings 完整生命周期流转图

.env 与环境变量 ​

.env 文件 ​

.env 是常见的本地配置文件,内容大致这样写:

txt
APP_NAME=Article App
ENV=dev
DEBUG=true
PORT=8000
DATA_DIR=./data

每一行都是:

text
配置名=配置值

本地开发和课程演示时很方便。

SettingsConfigDict

SettingsConfigDict 用来配置读取方式。本章先用这个设置:

python
model_config = SettingsConfigDict(env_file=".env")

创建 Settings() 时,它会读取 .env。如果环境变量和 .env 中有同名配置,环境变量优先。示例先在当前 Python 进程里移除 PORT,让第一次读取拿到 .env 中的 8000;随后设为 9000,重新创建对象观察覆盖结果。os.environ 的改动只影响这个进程,不会修改启动命令所在终端的环境;其他同名环境变量仍可能覆盖 .env 的对应值。

准备示例文件

将下面两份内容分别保存为 .code/main.py 和 .code/.env:

py
import os
from pathlib import Path

from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_file=Path(__file__).with_name(".env"))

    app_name: str
    env: str = "dev"
    debug: bool = False
    port: int = 8000
    data_dir: str = "./data"


# 先观察 .env 中的端口,再演示环境变量覆盖。
os.environ.pop("PORT", None)
settings = Settings()

print(settings.app_name)
print(settings.env)
print(settings.debug, type(settings.debug))
print(settings.port, type(settings.port))
print(settings.data_dir)

os.environ["PORT"] = "9000"
overridden_settings = Settings()
print("环境变量覆盖后的端口:", overridden_settings.port)
text
APP_NAME=Article App
ENV=dev
DEBUG=true
PORT=8000
DATA_DIR=./data

运行:

bash
uv run python main.py
txt
APP_NAME=Article App
ENV=dev
DEBUG=true
PORT=8000
DATA_DIR=./data

Settings 模型定义 ​

env_file 写相对路径时,会按运行命令时的当前目录查找。这里改用脚本路径构造 .env 的路径,所以从别的目录运行也能找到和 main.py 放在一起的文件。

python
from pathlib import Path

from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_file=Path(__file__).with_name(".env"))

    app_name: str
    env: str = "dev"
    debug: bool = False
    port: int = 8000
    data_dir: str = "./data"

配置读取与环境变量覆盖 ​

python
import os

os.environ.pop("PORT", None)
settings = Settings()

print(settings.app_name)
print(settings.env)
print(settings.debug, type(settings.debug))
print(settings.port, type(settings.port))
print(settings.data_dir)

os.environ["PORT"] = "9000"
overridden_settings = Settings()
print("环境变量覆盖后的端口:", overridden_settings.port)

没有其他同名环境变量干扰时,关键输出是:

text
True <class 'bool'>
8000 <class 'int'>
环境变量覆盖后的端口: 9000

第二次创建 Settings() 时,环境变量里的字符串 "9000" 会先转换成整数,再覆盖 .env 里的 PORT=8000。注意这三个对应关系:

字段名称对应

.env 中的名称会对应到模型字段,例如:

txt
APP_NAME=Article App

对应:

python
app_name: str

布尔值转换

布尔值会按字段类型转换:

text
"true" -> True

整数转换

text
"8000" -> 8000

.env 读取流程

BaseSettings 多源配置装载与覆盖优先级瀑布

配置值类型错误 ​

类型转换与校验

.env 中的值本质上是文本。比如:

txt
DEBUG=true
PORT=8000

模型里如果声明为:

python
debug: bool
port: int

Pydantic 会尝试转换成相应类型:

text
"true" -> True
"8000" -> 8000

转换不了就会抛出 ValidationError。这一步把 .env 中的 PORT=8000 改成 PORT=abc,看看错误:

将下面两份内容分别保存为 .code/main.py 和 .code/.env:

py
import os
from pathlib import Path

from pydantic import ValidationError
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_file=Path(__file__).with_name(".env"))

    app_name: str
    port: int


# 避免当前进程已有的 PORT 覆盖 .env 中故意写错的值。
os.environ.pop("PORT", None)

try:
    settings = Settings()
    print(settings.port)
except ValidationError as error:
    print(error)
    print("错误类型:", error.errors()[0]["type"])
text
APP_NAME=Article App
PORT=abc

运行:

bash
uv run python main.py

正常值是:

txt
PORT=8000

把它改成:

txt
PORT=abc

读取方式和上一节一样;为了聚焦端口,这里只保留 app_name 和 port:

python
from pathlib import Path

from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_file=Path(__file__).with_name(".env"))

    app_name: str
    port: int

再在 main.py 里捕获配置错误:

python
import os
from pydantic import ValidationError

os.environ.pop("PORT", None)

try:
    settings = Settings()
    print(settings.port)
except ValidationError as error:
    print(error)
    print("错误类型:", error.errors()[0]["type"])

os.environ.pop("PORT", None) 会清除当前示例进程中的同名变量,避免它盖过 .env 里故意写错的值。输出中的关键行是:

text
错误类型: int_parsing

配置读进来后仍要通过类型校验,因此:

text
PORT=abc

无法转换成 int,错误会在创建配置对象时出现。

类型错误流程

Settings 校验拦截与 Fast-Fail 启动防护机制

必填配置缺失 ​

值的格式正确还不够,必填配置也可能缺失。这一步故意不在 .env 中提供 APP_NAME:

将下面两份内容分别保存为 .code/main.py 和 .code/.env:

py
import os
from pathlib import Path

from pydantic import ValidationError
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_file=Path(__file__).with_name(".env"))

    app_name: str
    debug: bool = False


# 避免当前进程已有的 APP_NAME 填补 .env 中故意留空的必填项。
os.environ.pop("APP_NAME", None)

try:
    settings = Settings()
    print(settings)
except ValidationError as error:
    print(error)
    print("错误类型:", error.errors()[0]["type"])
text
DEBUG=true

运行:

bash
uv run python main.py

正常情况下还应有:

txt
APP_NAME=...

而 Settings 类里这样写:

python
from pathlib import Path

from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_file=Path(__file__).with_name(".env"))

    app_name: str
    debug: bool = False

app_name 没有默认值:

python
app_name: str

因此,如果 .env 和环境变量都没有提供它,创建配置对象时:

python
settings = Settings()

就会报错。示例先执行 os.environ.pop("APP_NAME", None),避免已有环境变量替它补值。关键错误类型是:

text
错误类型: missing

必填项与默认值对比

Settings 校验拦截与 Fast-Fail 启动防护机制

记住默认值和必填项的区别:

text
有默认值 -> 可选
没默认值 -> 必填

应用配置综合案例 ​

最后把本章主线合到一个小例子里。

.env 配置文件 ​

将下面两份内容分别保存为 .code/main.py 和 .code/.env:

py
from pathlib import Path

from pydantic import ValidationError
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_file=Path(__file__).with_name(".env"))

    app_name: str
    env: str = "dev"
    debug: bool = False
    port: int = 8000
    data_dir: str = "./data"


try:
    settings = Settings()
    print(settings.app_name)
    print(settings.env)
    print(settings.debug, type(settings.debug))
    print(settings.port, type(settings.port))
    print(settings.data_dir)
except ValidationError as error:
    print("配置读取失败")
    print(error)
text
APP_NAME=Article App
ENV=dev
DEBUG=true
PORT=8000
DATA_DIR=./data

运行:

bash
uv run python main.py
txt
APP_NAME=Article App
ENV=dev
DEBUG=true
PORT=8000
DATA_DIR=./data

Settings 模型定义 ​

python
from pathlib import Path

from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_file=Path(__file__).with_name(".env"))

    app_name: str
    env: str = "dev"
    debug: bool = False
    port: int = 8000
    data_dir: str = "./data"

配置读取与结果 ​

python
from pydantic import ValidationError
try:
    settings = Settings()
    print(settings.app_name)
    print(settings.env)
    print(settings.debug, type(settings.debug))
    print(settings.port, type(settings.port))
    print(settings.data_dir)
except ValidationError as error:
    print("配置读取失败")
    print(error)

运行结果

如果没有同名环境变量覆盖 .env,正常运行会看到:

text
Article App
dev
True <class 'bool'>
8000 <class 'int'>
./data

Settings() 会读取配置,并把 .env 中的文本转换成字段类型。这里展示成功路径;PORT=abc 和缺少 APP_NAME 的失败情况已经在前面分别演示。

综合案例流程

应用启动阶段 Settings 完整生命周期流转图

配置模型与业务模型

最后把配置模型和业务模型区分开:

BaseModel 业务模型 vs BaseSettings 配置模型对比架构

BaseModel 通常描述业务数据,BaseSettings 通常管理程序配置。

本章小结 ​

继承 BaseSettings 后,创建配置对象时就能读取 .env 和环境变量,并按字段类型转换、校验。SettingsConfigDict(env_file=...) 指定文件,同名环境变量优先。字段有默认值可以不提供;没有默认值就必须提供,缺失或无效都会通过 ValidationError 报出来。