还在手写 os.getenv?pydantic-settings 让你配置管理效率翻倍
线上接口刚起来就挂,日志里只有一句:
ValueError: invalid literal for int() with base 10: ''
我第一眼不会去翻业务代码,先看配置。
尤其是 Python 项目,十个里面有七八个会在启动脚本里塞一堆 os.getenv。刚开始看着挺轻,后面配置一多,味道就出来了。
import osDB_HOST = os.getenv("DB_HOST", "127.0.0.1")
DB_PORT = int(os.getenv("DB_PORT", "5432"))
REDIS_TTL = int(os.getenv("REDIS_TTL", "300"))
DEBUG = os.getenv("DEBUG", "false") == "true"
这段代码最麻烦的地方,不是丑。
是它太“相信环境变量”了。
DB_PORT="" 会炸。
DEBUG=True 不生效。
REDIS_TTL=abc 到运行时才报错。
线上容器里少挂了一个变量,你可能要等接口请求打进来才知道。
这种配置,我现在一般不手写了,直接用 pydantic-settings 收掉。
安装:
pip install pydantic-settings
配置文件先放一个 .env,本地开发够用了:
APP_NAME=order-api
APP_ENV=devDB_HOST=127.0.0.1
DB_PORT=5432
DB_USER=order_rw
DB_PASSWORD=local_pass
DB_NAME=order_center
REDIS_URL=redis://127.0.0.1:6379/0
REQUEST_TIMEOUT=2.5
ENABLE_TRACE=true
然后写一个配置类,不要散在各个模块里到处读环境变量。
from functools import lru_cache
from pydantic import Field, SecretStr, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDictclassAppSettings(BaseSettings):
app_name: str = "demo-api"
app_env: str = Field(default="dev", pattern="^(dev|test|prod)$")
db_host: str
db_port: int = Field(default=5432, ge=1, le=65535)
db_user: str
db_password: SecretStr
db_name: str
redis_url: str
request_timeout: float = Field(default=3.0, gt=0, le=30)
enable_trace: bool = False
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
extra="ignore",
)
@field_validator("db_host")
@classmethod
defdb_host_not_blank(cls, value: str) -> str:
value = value.strip()
ifnot value:
raise ValueError("DB_HOST 不能为空")
return value
@property
defdb_dsn(self) -> str:
pwd = self.db_password.get_secret_value()
return (
f"postgresql://{self.db_user}:{pwd}"
f"@{self.db_host}:{self.db_port}/{self.db_name}"
)
@lru_cache
defget_settings() -> AppSettings:
return AppSettings()
这段代码的好处很直接。
配置读进来以后,类型已经处理完了。DB_PORT 是 int,ENABLE_TRACE 是 bool,REQUEST_TIMEOUT 是 float。
不是你自己一行一行转。
我比较在意的是启动失败要早。
比如把 .env 里的端口写错:
DB_PORT=abc
启动时会直接报:
db_port
Input should be a valid integer
这个错误比接口跑到一半再炸舒服太多。
配置这种东西,越早死越好。别等请求进来,别等定时任务跑起来,更别等半夜报警把人叫醒。
在业务里用的时候,也别每个文件都 AppSettings() 一次。
settings = get_settings()defbuild_db_pool_config() -> dict:
return {
"dsn": settings.db_dsn,
"min_size": 2,
"max_size": 10,
"command_timeout": settings.request_timeout,
}
defshould_record_trace() -> bool:
return settings.app_env != "dev"and settings.enable_trace
这里用了 lru_cache,配置只会初始化一次。
不要小看这个细节。有些项目喜欢在函数里反复 new 配置类,跑单测时没感觉,线上频繁调用就很烦。配置不是业务数据,没必要每次重新解析一遍。
还有一个坑,密码不要随手打印。
SecretStr 不是摆设。
settings = get_settings()print(settings.db_password)
输出大概是:
**********
当然,你真要拼接 DSN,还是能通过 get_secret_value() 拿到原值。这个设计比较合适:默认不暴露,需要用的时候你自己明确取。
我见过不少 Python 项目,启动日志里直接把全量配置 dump 出来,数据库密码、Redis 密码、第三方 token 全在里面。排查问题是方便了,安全也基本没了。
还有环境变量命名问题。
业务里一般喜欢大写下划线,比如 DB_HOST,代码里喜欢小写字段,比如 db_host。pydantic-settings 默认能处理这种映射,够用。
如果你们公司变量名已经乱了,比如历史包袱里叫 PG_HOST,新服务又想叫 DB_HOST,可以这样兜一下:
from pydantic import AliasChoicesclassDbOnlySettings(BaseSettings):
host: str = Field(validation_alias=AliasChoices("DB_HOST", "PG_HOST"))
port: int = Field(default=5432, validation_alias=AliasChoices("DB_PORT", "PG_PORT"))
model_config = SettingsConfigDict(env_file=".env")
这东西我一般只在迁移期用。
别长期兼容一堆名字。配置名越多,越没人知道线上到底读的是哪个。
最后再看一段入口代码。
defboot_check() -> None:
settings = get_settings() print(
f"boot app={settings.app_name}, "
f"env={settings.app_env}, "
f"db={settings.db_host}:{settings.db_port}, "
f"trace={settings.enable_trace}"
)
if __name__ == "__main__":
boot_check()
生产环境里,我会在应用启动阶段就跑一次配置加载。
能启动,说明配置基本过了类型校验和规则校验。
不能启动,就让它直接失败。
比起手写一屏 os.getenv,pydantic-settings 真正省的不是那几行代码,而是后面排查配置问题的时间。
配置管理最怕的不是复杂,是散。
一个模块读 os.getenv("DB_HOST"),另一个模块读 os.environ["DATABASE_HOST"],还有一个地方自己写默认值。等线上出问题,连当前服务到底用了哪份配置都说不清。
把配置收进一个 Settings 类里,类型、默认值、校验、敏感字段,一次性放干净。
这事不花哨,但很值。