LoginSignup
2
0

Pydantic Settingsのすゝめ

Last updated at Posted at 2024-05-18

はじめに

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

1. 普通にOSの環境変数を取得する

$ export DB_USER=admin
$ export FLAG=True
$ export COUNT=1
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ライブラリで読み込む方法です。

.env
DB_USER=hoge
FLAG=True
COUNT=1
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型など)として扱う場合、以下のようにキャストが必要になります。

flag = boolean(os.getenv("FLAG"))
count = int(os.getenv("COUNT"))

また、課題1と被りますが、環境変数に予期せぬ値が設定されている場合、キャストが失敗してエラーが発生する可能性があるため、キャストをする前にもチェック処理が必要になったりします。

【課題3】 環境変数の存在チェックが必要

例えば、DB_USERの環境変数値が設定されていないときに例外をスローしたい場合、以下のような分岐処理を書く必要があります。

if os.getenv("DB_USER") is None:
    raise ValueError("DB_USER is not set")

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

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

Pydantic Settingsとは?

Pydantic Settingsは、Pydanticライブラリの一部であり、設定管理を型安全かつ簡潔に行うための強力なツールです。

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

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

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

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

前提条件

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

Pydantic Settingsを使ってみよう

それでは、Pydantic Settingsの基本的な使い方を紹介します。

まず、Pydantic Settingsを以下のようにインストールします。

pip install pydantic-settings

環境変数の設定モデル

まずは以下のようにOSの環境変数をセットしておきます。

$ export DB_USER=admin
$ export FLAG=True
$ export COUNT=1

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

settings.py
from pydantic_settings import BaseSettings

# 設定モデルを定義
class Settings(BaseSettings):
    DB_USER: str
    FLAG: bool
    COUNT: int

この設定モデルを使って環境変数を読み込むには、以下のようにします。

main.py
from settings import Settings

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

環境変数の型が誤っている場合

例えば、整数値を期待するCOUNTにstr型の値を渡してみると以下の例外が発生します。

$ export COUNT=test
$ 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が提供するエラーメッセージを見ることで、問題の原因を特定しやすくなっています。

デフォルト値をセットする

環境変数が設定されていない場合にデフォルト値を使用したい場合は、以下のようにフィールドにデフォルト値を指定します。

settings.py
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    DB_USER: str = "user" # デフォルト値
    FLAG: bool
    COUNT: int

なお、デフォルト値の型がフィールドの型と一致しない場合、バリデーションエラーが発生します。

環境変数の値が不足している場合

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

from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    DB_USER: str  # デフォルト値がないので指定必須になる
    FLAG: bool = False
    COUNT: int = 0
# 環境変数`DB_USER`を削除
$ unset DB_USER

この状態で実行すると、以下の通りDB_USERが見つけられない旨のエラーが表示されます。

$ 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を代入すると、設定モデルの振る舞いをカスマイズすることができます。

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

OSの環境変数と.envファイルの両方に同じ環境変数が定義されている場合、どっちが読み込まれる?

結論、OSの環境変数が読み込まれます。
Pydantic Settingsは以下の優先順位で環境変数を読み込み、最初の項目が最も優先されます。

  1. OSの環境変数
  2. .envファイルに定義された環境変数

(参考)
https://docs.pydantic.dev/latest/concepts/pydantic_settings/#changing-priority

.envファイルに意図しない環境変数が定義されている場合

ここで、設定モデルに定義されていない環境変数を.envファイルに書いてみましょう。

.env
DB_USER=hoge
FLAG=True
COUNT=1
HOGE=FUGA  # 追加

この状態でアプリケーションを実行すると、以下のエラーが発生します。

$ 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が設定モデルに定義されていない環境変数を検出し、バリデーションエラーを発生させたためです。
デフォルトでは、設定モデルに定義されていない環境変数は許可されません。

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

意図しない環境変数を許可したい場合

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

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では、環境ごとに異なる環境変数ファイルを使い分けることができます。
以下のように、SettingsConfigDictenv_file属性にタプルで複数のファイルパスを指定します。

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_sensitiveTrueに設定することで、環境変数名の大文字と小文字を区別することができます。

.env
db_user=hoge # 小文字にする
FLAG=True
COUNT=1
HOGE=FUGA
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に対応する値がないと怒られます。

$ 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()だと、安全性の欠如やエラーハンドリングの複雑さ等、いくつかの課題がありましたが、
これらを一気に解決できる強力なツールなので皆さんも是非使ってみてください。

なお、本記事では紹介できませんでしたが、以下のようなこともできるので興味のある人は試してみてください。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0