# 

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

&gt; ⏱️ **閱讀時間：** 15 分鐘
&gt; 🎯 **難度：** ⭐⭐ (基礎)

---

## 🤔 一句話解釋

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

---

## 🎯 為什麼需要設定管理？

### 常見問題

```python
# ❌ 硬編碼
DATABASE_URL = &#34;postgresql://user:password@localhost/db&#34;

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

# ❌ 沒有型別
DEBUG = os.getenv(&#34;DEBUG&#34;)  # 字串 &#34;true&#34;，不是 bool
```

### 使用 pydantic-settings

```python
# ✅ 型別安全 &#43; 自動驗證
from pydantic_settings import BaseSettings

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

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

---

## 📦 安裝

```bash
pip install pydantic-settings
```

---

## 🔧 基本用法

### 定義 Settings

```python
from pydantic_settings import BaseSettings

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

    # 選填（有預設值）
    debug: bool = False
    app_name: str = &#34;My App&#34;
    port: int = 8000

settings = Settings()
```

### 設定環境變數

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

或在 shell 設定：

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

### 環境變數命名

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

| Python 欄位 | 環境變數 |
|-------------|----------|
| `database_url` | `DATABASE_URL` |
| `secret_key` | `SECRET_KEY` |
| `debug` | `DEBUG` |

---

## 📁 讀取 .env 檔案

### 基本設定

```python
from pydantic_settings import BaseSettings, SettingsConfigDict

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

    model_config = SettingsConfigDict(
        env_file=&#34;.env&#34;,
        env_file_encoding=&#34;utf-8&#34;
    )
```

### 多環境設定

```python
from pydantic_settings import BaseSettings, SettingsConfigDict
import os

class Settings(BaseSettings):
    database_url: str
    secret_key: str
    debug: bool = False
    environment: str = &#34;development&#34;

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

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

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

---

## 🏷️ 環境變數前綴

```python
from pydantic_settings import BaseSettings, SettingsConfigDict

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

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

# 環境變數：
# MYAPP_DATABASE_URL=...
# MYAPP_REDIS_URL=...
# MYAPP_DEBUG=true
```

---

## 🔄 型別轉換

### 自動轉換

```python
from pydantic_settings import BaseSettings
from typing import List, Set

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

    # 數字
    port: int           # &#34;8000&#34; → 8000
    timeout: float      # &#34;30.5&#34; → 30.5

    # 列表（JSON 或逗號分隔）
    allowed_hosts: List[str]  # &#39;[&#34;host1&#34;,&#34;host2&#34;]&#39; 或 &#34;host1,host2&#34;
    cors_origins: Set[str]

# 環境變數
# DEBUG=true
# PORT=8000
# ALLOWED_HOSTS=[&#34;localhost&#34;,&#34;127.0.0.1&#34;]
# 或
# ALLOWED_HOSTS=localhost,127.0.0.1
```

### 複雜型別

```python
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=&#39;{&#34;host&#34;:&#34;localhost&#34;,&#34;port&#34;:5432,&#34;name&#34;:&#34;mydb&#34;,&#34;user&#34;:&#34;admin&#34;,&#34;password&#34;:&#34;secret&#34;}&#39;
```

### 使用前綴處理巢狀

```python
from pydantic_settings import BaseSettings, SettingsConfigDict

class DatabaseSettings(BaseSettings):
    host: str = &#34;localhost&#34;
    port: int = 5432
    name: str = &#34;mydb&#34;
    user: str = &#34;postgres&#34;
    password: str = &#34;&#34;

    model_config = SettingsConfigDict(env_prefix=&#34;DB_&#34;)

class RedisSettings(BaseSettings):
    host: str = &#34;localhost&#34;
    port: int = 6379
    db: int = 0

    model_config = SettingsConfigDict(env_prefix=&#34;REDIS_&#34;)

class Settings(BaseSettings):
    debug: bool = False

    @property
    def database(self) -&gt; DatabaseSettings:
        return DatabaseSettings()

    @property
    def redis(self) -&gt; 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

```python
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(&#39;**********&#39;)
print(settings.secret_key.get_secret_value()) # 實際的值

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

---

## 🚀 最佳實踐：完整設定範例

### settings.py

```python
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):
    &#34;&#34;&#34;應用程式設定&#34;&#34;&#34;

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

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

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

    # ===== Redis 設定 =====
    redis_url: str = &#34;redis://localhost:6379/0&#34;

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

    # ===== CORS 設定 =====
    cors_origins: List[str] = [&#34;http://localhost:3000&#34;]
    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 = &#34;ap-northeast-1&#34;
    s3_bucket: Optional[str] = None

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

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

    # ===== 計算屬性 =====
    @property
    def is_production(self) -&gt; bool:
        return self.environment == &#39;production&#39;

    @property
    def is_development(self) -&gt; bool:
        return self.environment == &#39;development&#39;

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


@lru_cache()
def get_settings() -&gt; Settings:
    &#34;&#34;&#34;獲取設定（使用快取）&#34;&#34;&#34;
    return Settings()


# 方便直接 import
settings = get_settings()
```

### .env.example

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

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

# 資料庫
DATABASE_URL=postgresql&#43;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
```

### 使用設定

```python
# 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)
```

---

## ⚙️ 依賴注入中使用

```python
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(&#34;/info&#34;)
async def info(settings: Settings = Depends(get_settings)):
    return {
        &#34;app_name&#34;: settings.app_name,
        &#34;debug&#34;: settings.debug,
        &#34;environment&#34;: settings.environment
    }
```

---

## ✅ 重點總結

### 基本設定

```python
from pydantic_settings import BaseSettings, SettingsConfigDict

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

    model_config = SettingsConfigDict(
        env_file=&#34;.env&#34;,
        env_prefix=&#34;MYAPP_&#34;,  # 可選
    )
```

### 環境變數對應

| 設定 | 環境變數 |
|------|----------|
| 欄位名自動轉大寫 | `debug` → `DEBUG` |
| 使用前綴 | `debug` &#43; prefix → `MYAPP_DEBUG` |
| case_sensitive=False | 不區分大小寫 |

### 型別轉換

| Python 型別 | 環境變數範例 |
|-------------|--------------|
| `bool` | `true`, `1`, `yes` |
| `int` | `8000` |
| `List[str]` | `a,b,c` 或 `[&#34;a&#34;,&#34;b&#34;]` |
| `SecretStr` | `secret` (會被遮罩) |

---

## 🎤 面試這樣答

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

**答案：**

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

---

**上一篇：** [02-5. 序列化與反序列化](./02-5)
**下一篇：** [02-7. Pydantic v1 vs v2 遷移指南](./02-7)

---

最後更新：2025-12-17


---

> 作者: luk  
> URL: https://yoru-karu-blog-lalaluk-52581ac5e0cef170a3c8922c19182ecb6f7bd604.gitlab.io/posts/tutorial/fastapi/02-6/  

