02-6. Settings 管理與環境變數

⏱️ 閱讀時間: 15 分鐘 🎯 難度: ⭐⭐ (基礎)


🤔 一句話解釋

pydantic-settings 讓你用型別安全的方式管理環境變數和設定檔。


🎯 為什麼需要設定管理?

常見問題

# ❌ 硬編碼
DATABASE_URL = "postgresql://user:password@localhost/db"

# ❌ 沒有驗證
import os
DATABASE_URL = os.getenv("DATABASE_URL")  # 可能是 None

# ❌ 沒有型別
DEBUG = os.getenv("DEBUG")  # 字串 "true",不是 bool

使用 pydantic-settings

# ✅ 型別安全 + 自動驗證
from pydantic_settings import BaseSettings

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

settings = Settings()  # 自動讀取環境變數

📦 安裝

pip install pydantic-settings

🔧 基本用法

定義 Settings

from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    # 必填(沒有預設值)
    database_url: str
    secret_key: str

    # 選填(有預設值)
    debug: bool = False
    app_name: str = "My App"
    port: int = 8000

settings = Settings()

設定環境變數

# .env 檔案
DATABASE_URL=postgresql://user:pass@localhost/db
SECRET_KEY=your-secret-key
DEBUG=true
APP_NAME=My Awesome App
PORT=3000

或在 shell 設定:

export DATABASE_URL=postgresql://user:pass@localhost/db
export SECRET_KEY=your-secret-key

環境變數命名

預設會把欄位名轉成大寫:

Python 欄位環境變數
database_urlDATABASE_URL
secret_keySECRET_KEY
debugDEBUG

📁 讀取 .env 檔案

基本設定

from pydantic_settings import BaseSettings, SettingsConfigDict

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

    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8"
    )

多環境設定

from pydantic_settings import BaseSettings, SettingsConfigDict
import os

class Settings(BaseSettings):
    database_url: str
    secret_key: str
    debug: bool = False
    environment: str = "development"

    model_config = SettingsConfigDict(
        # 根據環境載入不同的 .env
        env_file=(
            ".env",
            f".env.{os.getenv('ENVIRONMENT', 'development')}"
        ),
        env_file_encoding="utf-8",
        extra="ignore"  # 忽略 .env 中多餘的變數
    )

# .env.development
# DEBUG=true
# DATABASE_URL=postgresql://localhost/dev_db

# .env.production
# DEBUG=false
# DATABASE_URL=postgresql://prod-server/prod_db

🏷️ 環境變數前綴

from pydantic_settings import BaseSettings, SettingsConfigDict

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

    model_config = SettingsConfigDict(
        env_prefix="MYAPP_"  # 所有變數都要加前綴
    )

# 環境變數:
# MYAPP_DATABASE_URL=...
# MYAPP_REDIS_URL=...
# MYAPP_DEBUG=true

🔄 型別轉換

自動轉換

from pydantic_settings import BaseSettings
from typing import List, Set

class Settings(BaseSettings):
    # 布林值
    debug: bool  # "true", "1", "yes" → True

    # 數字
    port: int           # "8000" → 8000
    timeout: float      # "30.5" → 30.5

    # 列表(JSON 或逗號分隔)
    allowed_hosts: List[str]  # '["host1","host2"]' 或 "host1,host2"
    cors_origins: Set[str]

# 環境變數
# DEBUG=true
# PORT=8000
# ALLOWED_HOSTS=["localhost","127.0.0.1"]
# 或
# ALLOWED_HOSTS=localhost,127.0.0.1

複雜型別

from pydantic_settings import BaseSettings
from pydantic import BaseModel
from typing import Optional

class DatabaseConfig(BaseModel):
    host: str
    port: int
    name: str
    user: str
    password: str

class Settings(BaseSettings):
    # 巢狀設定可以用 JSON
    database: DatabaseConfig

# 環境變數(JSON 格式)
# DATABASE='{"host":"localhost","port":5432,"name":"mydb","user":"admin","password":"secret"}'

使用前綴處理巢狀

from pydantic_settings import BaseSettings, SettingsConfigDict

class DatabaseSettings(BaseSettings):
    host: str = "localhost"
    port: int = 5432
    name: str = "mydb"
    user: str = "postgres"
    password: str = ""

    model_config = SettingsConfigDict(env_prefix="DB_")

class RedisSettings(BaseSettings):
    host: str = "localhost"
    port: int = 6379
    db: int = 0

    model_config = SettingsConfigDict(env_prefix="REDIS_")

class Settings(BaseSettings):
    debug: bool = False

    @property
    def database(self) -> DatabaseSettings:
        return DatabaseSettings()

    @property
    def redis(self) -> RedisSettings:
        return RedisSettings()

# 環境變數
# DEBUG=true
# DB_HOST=localhost
# DB_PORT=5432
# DB_NAME=mydb
# DB_USER=admin
# DB_PASSWORD=secret
# REDIS_HOST=localhost
# REDIS_PORT=6379

🔒 敏感資料處理

使用 SecretStr

from pydantic_settings import BaseSettings
from pydantic import SecretStr

class Settings(BaseSettings):
    database_url: str
    secret_key: SecretStr  # 不會在日誌中顯示
    api_key: SecretStr

settings = Settings()

# 存取
print(settings.secret_key)                    # SecretStr('**********')
print(settings.secret_key.get_secret_value()) # 實際的值

# 在日誌中安全
import logging
logging.info(f"Settings: {settings}")  # secret_key 會被遮罩

🚀 最佳實踐:完整設定範例

settings.py

from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import SecretStr, field_validator, PostgresDsn
from typing import List, Optional
from functools import lru_cache

class Settings(BaseSettings):
    """應用程式設定"""

    # ===== 基本設定 =====
    app_name: str = "FastAPI App"
    version: str = "1.0.0"
    debug: bool = False
    environment: str = "development"

    # ===== 伺服器設定 =====
    host: str = "0.0.0.0"
    port: int = 8000
    workers: int = 1

    # ===== 資料庫設定 =====
    database_url: str
    database_pool_size: int = 5
    database_max_overflow: int = 10

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

    # ===== 安全設定 =====
    secret_key: SecretStr
    algorithm: str = "HS256"
    access_token_expire_minutes: int = 30
    refresh_token_expire_days: int = 7

    # ===== CORS 設定 =====
    cors_origins: List[str] = ["http://localhost:3000"]
    cors_allow_credentials: bool = True

    # ===== 外部服務 =====
    smtp_host: Optional[str] = None
    smtp_port: int = 587
    smtp_user: Optional[str] = None
    smtp_password: Optional[SecretStr] = None

    aws_access_key_id: Optional[str] = None
    aws_secret_access_key: Optional[SecretStr] = None
    aws_region: str = "ap-northeast-1"
    s3_bucket: Optional[str] = None

    # ===== 驗證 =====
    @field_validator('environment')
    @classmethod
    def validate_environment(cls, v: str) -> str:
        allowed = ['development', 'staging', 'production']
        if v not in allowed:
            raise ValueError(f'environment must be one of {allowed}')
        return v

    @field_validator('cors_origins', mode='before')
    @classmethod
    def parse_cors_origins(cls, v):
        if isinstance(v, str):
            return [origin.strip() for origin in v.split(',')]
        return v

    # ===== 計算屬性 =====
    @property
    def is_production(self) -> bool:
        return self.environment == 'production'

    @property
    def is_development(self) -> bool:
        return self.environment == 'development'

    # ===== 設定 =====
    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
        case_sensitive=False,
        extra="ignore"
    )


@lru_cache()
def get_settings() -> Settings:
    """獲取設定(使用快取)"""
    return Settings()


# 方便直接 import
settings = get_settings()

.env.example

# 基本設定
APP_NAME=My FastAPI App
DEBUG=false
ENVIRONMENT=development

# 伺服器
HOST=0.0.0.0
PORT=8000
WORKERS=4

# 資料庫
DATABASE_URL=postgresql+asyncpg://user:password@localhost:5432/mydb
DATABASE_POOL_SIZE=10
DATABASE_MAX_OVERFLOW=20

# Redis
REDIS_URL=redis://localhost:6379/0

# 安全
SECRET_KEY=your-super-secret-key-change-in-production
ACCESS_TOKEN_EXPIRE_MINUTES=30
REFRESH_TOKEN_EXPIRE_DAYS=7

# CORS
CORS_ORIGINS=http://localhost:3000,http://localhost:8080

# SMTP(可選)
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your-email@gmail.com
SMTP_PASSWORD=your-app-password

# AWS(可選)
AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret-key
AWS_REGION=ap-northeast-1
S3_BUCKET=my-bucket

使用設定

# main.py
from fastapi import FastAPI
from app.config import settings

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

# 在任何地方使用
from app.config import settings

print(settings.database_url)
print(settings.secret_key.get_secret_value())
print(settings.is_production)

⚙️ 依賴注入中使用

from fastapi import FastAPI, Depends
from functools import lru_cache
from app.config import Settings

app = FastAPI()

@lru_cache()
def get_settings():
    return Settings()

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

✅ 重點總結

基本設定

from pydantic_settings import BaseSettings, SettingsConfigDict

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

    model_config = SettingsConfigDict(
        env_file=".env",
        env_prefix="MYAPP_",  # 可選
    )

環境變數對應

設定環境變數
欄位名自動轉大寫debugDEBUG
使用前綴debug + prefix → MYAPP_DEBUG
case_sensitive=False不區分大小寫

型別轉換

Python 型別環境變數範例
booltrue, 1, yes
int8000
List[str]a,b,c["a","b"]
SecretStrsecret (會被遮罩)

🎤 面試這樣答

Q: 為什麼要用 pydantic-settings 而不是直接用 os.getenv?

答案:

  1. 型別安全:自動轉換和驗證型別(字串 “true” → bool True)
  2. 必填驗證:沒有預設值的欄位必須設定,啟動時就會報錯
  3. IDE 支援:有自動補全和型別提示
  4. 敏感資料:SecretStr 防止密碼洩漏到日誌
  5. 結構化:所有設定集中管理,不會散落各處

上一篇: 02-5. 序列化與反序列化 下一篇: 02-7. Pydantic v1 vs v2 遷移指南


最後更新:2025-12-17

0%