用 dataclass + Pydantic 做配置校验与序列化 用 dataclass Pydantic 做配置校验与序列化很多项目的配置管理是这样的:一个巨大的config.py,里面全是DB_HOST os.getenv(DB_HOST, localhost),端口用的时候临时int()一下,布尔值靠 true判断。跑起来没问题,直到某天线上把DB_PORT写成了5432 (多个空格),或者DEBUG1被当成字符串1永远为真——排查半天才发现是配置类型没校验。配置本质上是外部输入,和用户请求一样不可信。这篇讲怎么用dataclass和 Pydantic 给配置加上类型校验和序列化,让脏数据在启动时就报错,而不是在半夜的生产环境里发作。朴素写法的问题先看一段典型的手写配置:importosclassConfig:DB_HOSTos.getenv(DB_HOST,localhost)DB_PORTint(os.getenv(DB_PORT,5432))# 万一环境变量是空字符串,int() 直接崩DEBUGos.getenv(DEBUG,false)true# DEBUGTrue / DEBUG1 都会被判成 FalseMAX_CONNos.getenv(MAX_CONN)# 忘了转 int,后面拿去做数值比较就出错问题很集中:类型转换散落各处、没有统一校验、错误信息含糊(int()报的是invalid literal for int(),不告诉你是哪个配置项)。而且这种类没法方便地序列化成 dict 或 JSON。先用 dataclass 把结构立起来dataclass是标准库的方案,零依赖,适合结构清晰、校验需求简单的场景。它自动生成__init__、__repr__、__eq__:fromdataclassesimportdataclass,field,asdictdataclassclassDBConfig:host:strlocalhostport:int5432max_conn:int10# 可变默认值必须用 field(default_factory...),不能直接写 []tags:list[str]field(default_factorylist)cfgDBConfig(hostdb.internal,port6432)print(asdict(cfg))# 序列化成 dict:{host: db.internal, port: 6432, ...}但dataclass有个关键局限:它只做类型标注,不做类型校验。下面这行不会报错:cfgDBConfig(portnot a number)# 完全通过,port 现在是个字符串print(cfg.port1)# 用的时候才 TypeError,离出错现场很远了想让 dataclass 校验,得手写__post_init__:dataclassclassDBConfig:host:strlocalhostport:int5432def__post_init__(self):# __post_init__ 在 __init__ 之后自动调用,适合做校验和归一化ifnotisinstance(self.port,int):raiseTypeError(fport 必须是 int,收到{type(self.port).__name__})ifnot(1self.port65535):raiseValueError(fport 超出范围:{self.port})能用,但每个字段都手写校验很快就失控了。字段一多,__post_init__会变成一大坨 if。这时候该上 Pydantic。Pydantic:声明式校验 自动类型转换Pydantic 的核心价值是:你只声明类型,它负责校验和转换。而且转换是智能的——环境变量拿到的字符串6432会被自动转成 int。frompydanticimportBaseModel,Field,field_validatorclassDBConfig(BaseModel):host:strlocalhostport:intField(default5432,ge1,le65535)# ge/le 直接声明数值范围max_conn:intField(default10,gt0)tags:list[str][]# 字符串 6432 自动转 int,校验通过cfgDBConfig(port6432)print(cfg.port,type(cfg.port))# 6432 class int# 越界或转换失败,报错清晰地指出是哪个字段、错在哪try:DBConfig(port70000)exceptExceptionase:print(e)# port: Input should be less than or equal to 65535 ...需要自定义校验逻辑时,用field_validator,比 dataclass 的__post_init__清晰得多:classDBConfig(BaseModel):host:strlocalhostfield_validator(host)classmethoddefstrip_and_check(cls,v:str)-str:vv.strip()# 顺手去掉环境变量里常见的首尾空格ifnotv:raiseValueError(host 不能为空)returnvprint(DBConfig(host db.internal ).host)# db.internal,空格被清理直接从环境变量加载:pydantic-settings配置的来源通常是环境变量。Pydantic 官方的pydantic-settings能自动读环境变量并完成校验,这是它相比 dataclass 最实用的一步(需要pip install pydantic-settings):frompydantic_settingsimportBaseSettings,SettingsConfigDictfrompydanticimportFieldclassSettings(BaseSettings):model_configSettingsConfigDict(env_prefixAPP_,# 只读 APP_ 开头的环境变量env_file.env,# 也支持从 .env 文件读)db_host:strlocalhostdb_port:int5432debug:boolFalse# 1/true/yes 都能正确转成 Truemax_conn:intField(default10,gt0)# 假设环境里有 APP_DB_PORT6432 APP_DEBUGtruesettingsSettings()print(settings.db_port)# 6432(自动转 int)print(settings.debug)# True(智能解析布尔)关键收益:配置错误在Settings()实例化那一刻(应用启动时)就抛出,而不是等到运行时某段代码去用它。这就是让错误尽早暴露的价值。序列化:dump 出 dict 和 JSON配置经常要落盘、打日志或传给别的服务,Pydantic 的序列化开箱即用:settingsSettings(db_hostdb.internal,db_port6432)# 转 dictprint(settings.model_dump())# {db_host: db.internal, db_port: 6432, debug: False, max_conn: 10}# 转 JSON 字符串print(settings.model_dump_json(indent2))# 打日志时排除敏感字段print(settings.model_dump(exclude{db_password}))反过来,从 dict/JSON 恢复对象也很直接:Settings(**some_dict)或Settings.model_validate_json(json_str),而且恢复过程同样会走一遍完整校验。dataclass 还是 Pydantic?不是二选一,按场景选:纯内部数据结构、性能敏感、不接收外部输入:用dataclass。它更轻(标准库、无第三方依赖),实例化更快,适合大量创建的临时对象。配置、API 请求体、任何来自外部的输入:用 Pydantic。校验和类型转换是刚需,pydantic-settings直接对接环境变量,省掉大量样板代码。想要 Pydantic 的校验又想保持 dataclass 的写法?Pydantic 提供了pydantic.dataclasses.dataclass,是两者的折中。小结dataclass只标注类型不校验,校验得手写__post_init__,字段一多就失控;优点是零依赖、轻量。Pydantic声明式校验 自动类型转换,Field(ge, le, gt)声明约束,field_validator写自定义逻辑。pydantic-settings直接从环境变量/.env加载并校验,让配置错误在应用启动时就暴露。model_dump()/model_dump_json()开箱即用地序列化,还能exclude敏感字段。一句话记忆:配置是外部输入,别信它——用 Pydantic 在启动时把脏数据挡在门外。