Pydantic Settingsのすゝめ

# はじめに
Pythonのアプリケーションで環境変数を読み込むときってどうしてますか？
一般的に広く使われている方法としては`os.getenv()`を使った以下の2つだと思います。


**1. 普通にOSの環境変数を取得する**
```bash
$ export DB_USER=admin
$ export FLAG=True
$ export COUNT=1
```
```python
import os

print(os.getenv("DB_USER")) # admin
print(os.getenv("FLAG"))    # True
print(os.getenv("COUNT"))   # 1
```


**2.  `python-dotenv`を使って`.env`ファイルから取得する**
`.env`ファイル内に定義した環境変数の値を`python-dotenv`ライブラリで読み込む方法です。
```bash: .env
DB_USER=hoge
FLAG=True
COUNT=1
```

```python
from dotenv import load_dotenv
import os

# .envファイルを読み込む
load_dotenv()
print(os.getenv("DB_USER")) # admin
print(os.getenv("FLAG"))    # True
print(os.getenv("COUNT"))   # 1
```

# `os.getenv()`の課題
ただ、上記の`os.getenv()`で環境変数を読み込む場合、以下のような課題があります。

## 【課題1】 型安全性が欠如している
`os.getenv()`を使う場合、読み込む環境変数はすべて**文字列**として扱われます。
そのため、環境変数の型をチェックしたい場合、別途チェックするための処理を実装する必要があります。
これによってコードが複雑化し、メンテナンス性が下がる要因になります。

## 【課題2】 明示的なキャストが必要
読み込んだ環境変数をstr型以外（int型やboolean型など）として扱う場合、以下のようにキャストが必要になります。
```python
flag = boolean(os.getenv("FLAG"))
count = int(os.getenv("COUNT"))
```
また、課題1と被りますが、環境変数に予期せぬ値が設定されている場合、キャストが失敗してエラーが発生する可能性があるため、キャストをする前にもチェック処理が必要になったりします。

## 【課題3】 環境変数の存在チェックが必要
例えば、`DB_USER`の環境変数値が設定されていないときに例外をスローしたい場合、以下のような分岐処理を書く必要があります。
```python
if os.getenv("DB_USER") is None:
    raise ValueError("DB_USER is not set")
```

---

`os.getenv()`を利用する場合のデメリットをまとめると、**型安全性が欠如しているので予期せぬ不具合が発生するリスクがあり、また、環境変数の存在チェックやキャストなどの余計な処理が増えることでメンテナンス性が低下します。**

そこで、**Pydantic Settings** というライブラリを利用することで上記にあげたデメリットを解消し、より安全で可読性の高いコードを書くことができます。

# Pydantic Settingsとは？
https://docs.pydantic.dev/latest/concepts/pydantic_settings/

Pydantic Settingsは、[Pydantic](https://docs.pydantic.dev/latest/)ライブラリの一部であり、設定管理を型安全かつ簡潔に行うための強力なツールです。

:::note INFO
Pydantic自体はPythonのデータ検証とシリアル化のためのライブラリであり、型ヒントを使用してデータ構造を定義し、実行時にデータを検証することができるものです。
:::

Pydantic Settingsは、このPydanticの機能を活用して環境変数の読み込みや設定の管理を容易にします。

また、後述しますが、Pydantic Settingsは環境変数の設定をクラスとして定義することができます。
これによって、環境変数の読み込みと設定の管理がひとつのクラスにまとめられるため、設定に関するコードが散在することを防げるといったメリットもあります。

以降で簡単な使い方を紹介します。

# 前提条件
本記事で説明するPydantic Settingsは、2024年5月時点の最新バージョンである`2.2.1`を使います。
なお、pydanticの仕様はver1とver2で記述方法に違いがあるため、ver1を使っている方は注意してください。


# Pydantic Settingsを使ってみよう
それでは、Pydantic Settingsの基本的な使い方を紹介します。

まず、Pydantic Settingsを以下のようにインストールします。
```bash
pip install pydantic-settings
```

## 環境変数の設定モデル
まずは以下のようにOSの環境変数をセットしておきます。
```bash
$ export DB_USER=admin
$ export FLAG=True
$ export COUNT=1
```

Pydantic Settingsで環境変数を取り扱う場合、設定モデルというものを定義します。
以下のように`BaseSettings`を継承したクラスを定義し、環境変数名と対応する型を定義します。

```python: settings.py
from pydantic_settings import BaseSettings

# 設定モデルを定義
class Settings(BaseSettings):
    DB_USER: str
    FLAG: bool
    COUNT: int
```
この設定モデルを使って環境変数を読み込むには、以下のようにします。
```python: main.py
from settings import Settings

settings = Settings()
print(settings.DB_USER)  # admin
print(settings.FLAG)     # True
print(settings.COUNT)    # 1
```

### 環境変数の型が誤っている場合
例えば、整数値を期待する`COUNT`にstr型の値を渡してみると以下の例外が発生します。
```bash
$ export COUNT=test
```
```bash
$ python main.py
ValidationError
1 validation error for Settings
 COUNT
  Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='hoge', input_type=str]
    For further information visit https://errors.pydantic.dev/2.7/v/int_parsing
  File "<your-directory>/main.py", line 3, in <module>
    settings = Settings()
               ^^^^^^^^^^
```
Pydantic Settingsは設定モデルの名前で環境変数を読み込み、読み込んだ環境変数の値が定義された型に適合しているかどうかを検証します。
定義された型と異なる場合、Pydanticが例外を発生させてくれます。

上記のエラーメッセージを見ると、`COUNT`の値が整数として解釈できなかったことがわかります。
めちゃくちゃわかりやすくないですか？
Pydanticが提供するエラーメッセージを見ることで、問題の原因を特定しやすくなっています。


## デフォルト値をセットする
環境変数が設定されていない場合にデフォルト値を使用したい場合は、以下のようにフィールドにデフォルト値を指定します。
```python: settings.py
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    DB_USER: str = "user" # デフォルト値
    FLAG: bool
    COUNT: int
```
なお、デフォルト値の型がフィールドの型と一致しない場合、バリデーションエラーが発生します。

### 環境変数の値が不足している場合
なお、デフォルト値を指定していないフィールドは**指定必須**になるので注意してください。
例えば、`DB_USER`を必須の環境変数とし、値が設定されていない場合にエラーを発生させたい場合は以下のようにします。
```python
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    DB_USER: str  # デフォルト値がないので指定必須になる
    FLAG: bool = False
    COUNT: int = 0
```
```bash
# 環境変数`DB_USER`を削除
$ unset DB_USER
```
この状態で実行すると、以下の通り`DB_USER`が見つけられない旨のエラーが表示されます。
```bash
$ python main.py  
Traceback (most recent call last):
...省略

DB_USER
  Field required [type=missing, input_value={'COUNT': '100', 'FLAG': 'True'}, input_type=dict]
    For further information visit https://errors.pydantic.dev/2.7/v/missing
```


## `.env`ファイルからの読み込み
`.env`ファイルから環境変数を読み込みたい場合、以下のように定義します。
※`model_config`変数に`SetitngsConfigDict`を代入すると、設定モデルの振る舞いをカスマイズすることができます。

```python: settings.py
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
    )

    DB_USER: str
    FLAG: bool
    COUNT: int
```

:::note warn
#### OSの環境変数と`.env`ファイルの両方に同じ環境変数が定義されている場合、どっちが読み込まれる？
[デフォルト](https://docs.pydantic.dev/latest/concepts/pydantic_settings/#field-value-priority)では、**OSの環境変数**が読み込まれます。

なお、[`BaseSettings.settings_customise_sources()`](https://docs.pydantic.dev/latest/concepts/pydantic_settings/#customise-settings-sources) を使えば順序は自由に変更することができます。
詳しくは本記事のコメント欄、@__Kat__ さんの解説をご参照ください。
https://qiita.com/inetcpl/items/b4146b9e8e1adad239d8#comment-5f747dba33ed8f7a5297



:::


### `.env`ファイルに意図しない環境変数が定義されている場合
ここで、設定モデルに定義されていない環境変数を`.env`ファイルに書いてみましょう。
```bash: .env
DB_USER=hoge
FLAG=True
COUNT=1
HOGE=FUGA  # 追加
```

この状態でアプリケーションを実行すると、以下のエラーが発生します。
```bash
$ python main.py
Traceback (most recent call last):
...省略

pydantic_core._pydantic_core.ValidationError: 1 validation error for Settings
 hoge
  Extra inputs are not permitted [type=extra_forbidden, input_value='FUGA', input_type=str]
    For further information visit https://errors.pydantic.dev/2.7/v/extra_forbidden
```
これは、Pydantic Settingsが設定モデルに定義されていない環境変数を検出し、バリデーションエラーを発生させたためです。
デフォルトでは、**設定モデルに定義されていない環境変数は許可されません。**

https://docs.pydantic.dev/latest/concepts/pydantic_settings/#dotenv-env-support

これによって、タイポや予期せぬ環境変数の混入を防ぐことができます。

#### 意図しない環境変数を許可したい場合
ただし、状況によっては設定モデルに定義されていない環境変数を許可したい場合もあると思います。
その場合は、以下のように`SettingsConfigDict`のextra属性を`ignore`に設定します。

```python: settings.py
class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
        extra="ignore", # 追加
    )

    DB_USER: str
    FLAG: bool
    COUNT: int
```

### `.env`ファイルは複数読み込ませることができる
Pydantic Settingsでは、環境ごとに異なる環境変数ファイルを使い分けることができます。
以下のように、`SettingsConfigDict`の`env_file`属性にタプルで複数のファイルパスを指定します。
```python: settings.py
class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        env_file=('.env', '.env.prod')
    )
```
上記の例では、`.env`と`.env.prod`の2つのファイルを指定しています。
Pydantic Settingsは、タプルで指定された順番に環境変数ファイルを読み込み、**後から読み込まれたファイルの環境変数が優先されます。**
つまり、上記の例では、`.env.prod`に定義された環境変数が`.env`に定義された環境変数よりも優先されます。
これのユースケースとしては、開発環境と本番環境で異なる環境変数を使い分けに活用できます。

## 環境変数の大文字と小文字を区別する
Pydantic Settingsはデフォルトの動作として、**環境変数名の大文字と小文字を区別しません。**
ただし、以下のように`case_sensitive`を`True`に設定することで、環境変数名の大文字と小文字を区別することができます。
```bash: .env
db_user=hoge # 小文字にする
FLAG=True
COUNT=1
HOGE=FUGA
```
```python: settings.py
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
        extra="ignore",
        case_sensitive=True,  # 追加
    )
    
    DB_USER: str
    FLAG: bool
    COUNT: int
```
上記を実行すると、以下のエラーが発生し、`DB_USER`に対応する値がないと怒られます。
```bash
$ python main.py
Traceback (most recent call last):
... 省略

pydantic_core._pydantic_core.ValidationError: 1 validation error for Settings
DB_USER
  Field required [type=missing, input_value={'COUNT': '100', 'FLAG': ... 'hoge', 'HOGE': 'FUGA'}, input_type=dict]
    For further information visit https://errors.pydantic.dev/2.7/v/missing
```

# さいごに
Pydantic Settingsどうでしょうか？
従来の`os.getenv()`だと、安全性の欠如やエラーハンドリングの複雑さ等、いくつかの課題がありましたが、
これらを一気に解決できる強力なツールなので皆さんも是非使ってみてください。

なお、本記事では紹介できませんでしたが、以下のようなこともできるので興味のある人は試してみてください。
- [DockerのSecretsから値を読み込む](https://docs.pydantic.dev/latest/concepts/pydantic_settings/#use-case-docker-secrets)
- [環境変数にエイリアスを設定する](https://docs.pydantic.dev/latest/concepts/pydantic_settings/#validation-of-default-values)