はじめに
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
ライブラリで読み込む方法です。
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
を継承したクラスを定義し、環境変数名と対応する型を定義します。
from pydantic_settings import BaseSettings
# 設定モデルを定義
class Settings(BaseSettings):
DB_USER: str
FLAG: bool
COUNT: int
この設定モデルを使って環境変数を読み込むには、以下のようにします。
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が提供するエラーメッセージを見ることで、問題の原因を特定しやすくなっています。
デフォルト値をセットする
環境変数が設定されていない場合にデフォルト値を使用したい場合は、以下のようにフィールドにデフォルト値を指定します。
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
を代入すると、設定モデルの振る舞いをカスマイズすることができます。
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は以下の優先順位で環境変数を読み込み、最初の項目が最も優先されます。
- OSの環境変数
- .envファイルに定義された環境変数
(参考)
https://docs.pydantic.dev/latest/concepts/pydantic_settings/#changing-priority
.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
に設定します。
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
属性にタプルで複数のファイルパスを指定します。
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
に設定することで、環境変数名の大文字と小文字を区別することができます。
db_user=hoge # 小文字にする
FLAG=True
COUNT=1
HOGE=FUGA
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()
だと、安全性の欠如やエラーハンドリングの複雑さ等、いくつかの課題がありましたが、
これらを一気に解決できる強力なツールなので皆さんも是非使ってみてください。
なお、本記事では紹介できませんでしたが、以下のようなこともできるので興味のある人は試してみてください。