1. 告别手写os.getenv的时代

在Python项目中,配置管理一直是个让人头疼的问题。记得我刚入行时,项目里到处都是这样的代码:

import os

DB_HOST = os.getenv('DB_HOST', 'localhost')
DB_PORT = int(os.getenv('DB_PORT', '5432'))
DEBUG = os.getenv('DEBUG', 'False').lower() == 'true'

这种写法至少有三大痛点:

  1. 类型转换需要手动处理(比如上面的int转换)
  2. 默认值分散在各处,难以统一管理
  3. 配置项多了之后,代码会变得冗长且难以维护

2. pydantic-settings核心功能解析

2.1 基础环境变量加载

pydantic-settings最基础的用法是通过继承BaseSettings类:

from pydantic import BaseModel
from pydantic_settings import BaseSettings

class DatabaseConfig(BaseModel):
    host: str = 'localhost'
    port: int = 5432

class Settings(BaseSettings):
    db: DatabaseConfig
    debug: bool = False

这样就能自动从环境变量加载配置:

  • DB_HOST → settings.db.host
  • DB_PORT → settings.db.port
  • DEBUG → settings.debug

2.2 嵌套配置与自动类型转换

pydantic-settings的强大之处在于对复杂嵌套配置的支持:

class AuthConfig(BaseModel):
    secret_key: str
    algorithm: str = 'HS256'
    expire_minutes: int = 30

class Settings(BaseSettings):
    database: DatabaseConfig
    auth: AuthConfig
    logging_level: str = 'INFO'

环境变量会自动映射为:

  • DATABASE_HOST
  • DATABASE_PORT
  • AUTH_SECRET_KEY
  • AUTH_ALGORITHM
  • AUTH_EXPIRE_MINUTES
  • LOGGING_LEVEL

3. 高级配置技巧

3.1 多环境配置管理

实际项目中我们通常需要区分不同环境:

class Settings(BaseSettings):
    env_name: str = 'dev'
    
    class Config:
        env_file = '.env'
        env_file_encoding = 'utf-8'
        env_nested_delimiter = '__'
        
    @property
    def is_prod(self) -> bool:
        return self.env_name == 'prod'

通过.env文件管理不同环境的配置:

# .env.dev
DATABASE_HOST=localhost
DATABASE_PORT=5432

# .env.prod  
DATABASE_HOST=db.prod.com
DATABASE_PORT=5432

3.2 敏感信息处理

对于密码等敏感信息,pydantic-settings提供了SecretStr类型:

from pydantic import SecretStr

class Settings(BaseSettings):
    db_password: SecretStr
    
    class Config:
        secrets_dir = '/run/secrets'

这样密码可以存储在单独的文件中:

# /run/secrets/db_password
my_super_secret_password

4. 实战案例:Web应用配置

4.1 完整配置示例

from pydantic import BaseModel, SecretStr
from pydantic_settings import BaseSettings

class DatabaseConfig(BaseModel):
    host: str = 'localhost'
    port: int = 5432
    user: str = 'postgres'
    password: SecretStr
    name: str = 'app_db'

class RedisConfig(BaseModel):
    host: str = 'localhost'
    port: int = 6379
    db: int = 0

class AuthConfig(BaseModel):
    secret_key: SecretStr
    algorithm: str = 'HS256'
    access_token_expire: int = 30  # minutes

class Settings(BaseSettings):
    debug: bool = False
    database: DatabaseConfig
    redis: RedisConfig
    auth: AuthConfig
    
    class Config:
        env_file = '.env'
        env_nested_delimiter = '__'
        secrets_dir = '/run/secrets'

4.2 配置使用示例

from fastapi import FastAPI
from .config import Settings

settings = Settings()
app = FastAPI(debug=settings.debug)

@app.get("/info")
async def info():
    return {
        "db_host": settings.database.host,
        "redis_port": settings.redis.port,
        "token_algo": settings.auth.algorithm
    }

5. 常见问题解决方案

5.1 环境变量命名冲突

当多个配置项可能重名时,可以通过前缀解决:

class Settings(BaseSettings):
    class Config:
        env_prefix = 'APP_'

这样环境变量需要以APP_开头:

  • APP_DATABASE_HOST
  • APP_REDIS_HOST

5.2 自定义环境变量名

对于某些特殊环境变量名,可以使用Field的alias:

from pydantic import Field

class Settings(BaseSettings):
    db_host: str = Field(..., alias='DATABASE_SERVER')

5.3 配置验证

pydantic的验证器同样适用:

from pydantic import validator

class Settings(BaseSettings):
    port: int
    
    @validator('port')
    def validate_port(cls, v):
        if not 1024 <= v <= 65535:
            raise ValueError('Port must be between 1024 and 65535')
        return v

6. 性能优化建议

6.1 避免重复加载

在Web应用中,通常只需要加载一次配置:

# config.py
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    ...

settings = Settings()

# app.py
from .config import settings

6.2 延迟加载

对于测试等场景,可以延迟加载配置:

class LazySettings:
    _instance = None
    
    def __init__(self):
        if not self._instance:
            self._instance = Settings()
    
    def __getattr__(self, name):
        return getattr(self._instance, name)

settings = LazySettings()

7. 测试策略

7.1 单元测试配置

import os
from unittest import TestCase

class TestConfig(TestCase):
    def setUp(self):
        os.environ['DATABASE_HOST'] = 'test.db'
        os.environ['DATABASE_PORT'] = '5432'
        
    def test_config(self):
        settings = Settings()
        self.assertEqual(settings.database.host, 'test.db')
        self.assertEqual(settings.database.port, 5432)

7.2 使用pytest fixture

import pytest
from pydantic_settings import BaseSettings

@pytest.fixture
def test_settings():
    class TestSettings(BaseSettings):
        db_host: str = 'localhost'
        
    return TestSettings()

def test_db_host(test_settings):
    assert test_settings.db_host == 'localhost'

8. 与传统方案的对比

8.1 与python-dotenv对比

特性 python-dotenv pydantic-settings
类型转换 需要手动处理 自动类型转换
嵌套配置 不支持 完善支持
环境变量优先级 可自定义
敏感信息处理 无特殊支持 内置SecretStr
配置验证 内置验证器

8.2 与Django配置对比

Django的配置系统虽然强大,但存在以下问题:

  1. 全局单例模式,难以测试
  2. 缺乏类型提示
  3. 配置分散在settings.py和环境变量中

pydantic-settings在这些方面都有明显优势。

9. 迁移指南

9.1 从os.getenv迁移

  1. 识别项目中所有的os.getenv调用
  2. 创建对应的pydantic模型
  3. 逐步替换,保持向后兼容:
# 旧代码
DB_HOST = os.getenv('DB_HOST', 'localhost')

# 过渡方案
class Settings(BaseSettings):
    db_host: str = 'localhost'

settings = Settings()
DB_HOST = settings.db_host  # 兼容旧代码

9.2 从ini/json配置迁移

  1. 将现有配置转换为环境变量或.env文件
  2. 定义对应的pydantic模型
  3. 使用pydantic的解析方法加载旧配置:
import json
from pydantic import BaseModel

class OldConfig(BaseModel):
    database_host: str
    
    @classmethod
    def from_json(cls, path):
        with open(path) as f:
            return cls(**json.load(f))

10. 最佳实践总结

  1. 环境区分 :使用不同.env文件管理各环境配置
  2. 敏感信息 :使用SecretStr和secrets_dir管理
  3. 配置验证 :充分利用pydantic的验证器
  4. 性能优化 :避免重复加载配置
  5. 文档生成 :利用Field的description自动生成配置文档
  6. 版本控制 :.env.example加入版本控制,.env加入.gitignore
  7. 监控配置 :对关键配置项添加监控和告警

一个完整的生产级配置示例:

from pydantic import BaseModel, Field, SecretStr, validator
from pydantic_settings import BaseSettings
from typing import Literal

class DatabaseConfig(BaseModel):
    host: str = Field(..., description="Database server host")
    port: int = Field(5432, description="Database server port")
    user: str = Field('postgres', description="Database user")
    password: SecretStr = Field(..., description="Database password")
    pool_size: int = Field(10, description="Connection pool size")
    
    @validator('pool_size')
    def validate_pool_size(cls, v):
        if v < 1 or v > 100:
            raise ValueError('Pool size must be between 1 and 100')
        return v

class Settings(BaseSettings):
    env: Literal['dev', 'test', 'prod'] = Field('dev', description="Runtime environment")
    database: DatabaseConfig = Field(..., description="Database configuration")
    
    class Config:
        env_file = '.env'
        env_nested_delimiter = '__'
        secrets_dir = '/run/secrets'
        
    @property
    def is_prod(self) -> bool:
        return self.env == 'prod'

通过pydantic-settings,我们实现了:

  • 类型安全的配置管理
  • 自动化的环境变量加载
  • 完善的文档和验证
  • 优雅的敏感信息处理
  • 跨环境的统一配置接口
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐