😋

pydanticで環境変数の取り扱いを楽にする

2023/06/16に公開

Pythonで環境変数を読み込みたいときありますよね。
pydantic BaseSettingsを使うといい感じに読み取りができるので紹介したいと思います。

pydanticとは

公式から

Data validation and settings management using Python type annotations.
pydantic enforces type hints at runtime, and provides user friendly errors when data is invalid.
Define how data should be in pure, canonical Python; validate it with pydantic.

型アノテーションを使った検証や管理を行ってくれます。
Pythonは動的型付け言語のため、本来は型を意識しなくて良いですが
pydanticは型付け検証を行ってくれるため、静的型付けを意識した開発ができます。

検証で使用したバージョンです。

pydantic==1.10.9

BaseSettingsを使ってみる

基本的な使い方です。
BaseSettingsを継承してクラスを定義します。

from pydantic import BaseSettings, Field

class Settings(BaseSettings):
    sample: str | None = Field(default=None)

インスタンス変数sampleが環境変数名です。
sampleまたはSAMPLEの環境変数が設定されると値が自動的に設定されます。

Field関数は変数に対して検証の追加設定を記載できます。
ここではデフォルト値をNoneにするよう設定しています。

この状態で下記コードを実行してみます。

from pydantic import BaseSettings, Field


class Settings(BaseSettings):
    sample: str | None = Field(default=None)

# 環境変数なし
print(Settings().dict())
# 結果: {'sample': None}

# 環境変数を設定して出力
os.environ["sample"] = "settings_sample"
print(Settings().dict())
# 結果: {'sample': 'settings_sample'}

# 大文字で環境変数を設定
os.environ["SAMPLE"] = "SETTINGS_SAMPLE"
print(Settings().dict())
# 結果: {'sample': 'SETTINGS_SAMPLE'}

BaseSettingsはインスタンス生成後dict()メソッドでインスタンス変数名:値のdictを出力します。
環境変数設定前はNoneで出力されますが、設定後は値が出力されます。

インスタンス変数名と環境変数名を別にしたい

先程の例ではインスタンス変数sampleに対応する環境変数名は
sampleまたはSAMPLEでした。

Field属性のオプションを使えば
インスタンス変数名と環境変数名を別々にすることが可能です。


class ENVSettings(BaseSettings):
    api_key: str | None = Field(default=None, env="api_sample")

# 設定無し
print(ENVSettings().dict())
# {"api_key": None}

# envを指定するとenvの環境変数のみに対応されます。
os.environ["api_key"] = "aaaaabbbbbccccc"
print(ENVSettings().dict())
# {'api_key': None}

os.environ["api_sample"] = "dddddeeeeefffff"
print(ENVSettings().dict())
# {'api_key': 'dddddeeeeefffff'}

# 大文字も反映
os.environ["API_SAMPLE"] = "DDDDDEEEEEFFFFF"
print(ENVSettings().dict())
# {'api_key': 'DDDDDEEEEEFFFFF'}

Field関数にenvオプションを追加しました。
envオプションは指定したキーを環境変数とみなしてくれます。
上記例では、インスタンス変数api_keyは環境変数api_sampleによって値が設定されることになります。

大文字と小文字で区別したい

先程までは環境変数名が大文字でも小文字でも設定が行われました。
BaseSettingを使用すればで大文字と小文字の区別をすることが可能です。

class SettingsCase(BaseSettings):
    name: str | None = Field(default=None, env="NAME_KEY")

    class Config:
        # 小文字・大文字区別を行う
        case_sensitive = True


# 使用例
os.environ["name_key"] = "xxxxxyyyyyzzzzz"
print(SettingsCase().dict())
# envが大文字のため対応しない
# {'name': None}

os.environ["NAME_KEY"] = "XXXXXYYYYYZZZZZ"
print(SettingsCase().dict())
# {'name': 'XXXXXYYYYYZZZZZ'}

BaseSettingsを継承したクラスにConfigのサブクラスを定義します。
継承先でcase_sensitive = Trueにすると大文字・小文字の区別が行われます。
上記例だと、環境変数NAME_KEYは値が設定されますが、name_keyでは設定されません。

.envから環境変数を読み込みたい

実際の開発では.envなどの外部ファイルを使って環境変数の管理を行うと思います。

その場合は、pydantic[dotenv].envファイル内の環境変数を読み込んでくれます。
pydantic[dotenv]pydantic + python-dotenvの組み合わせで
いずれかの方法でインストールします。

pip install pydantic python-dotenv

# または
pip install pydantic[dotenv]

以下サンプルです。

.
├── .env
└── {実行ファイル}.py
.env
DOT_ENV="dot_sample"
class DotSettings(BaseSettings):
    dot_env: str = Field(default=None)

    class Config:
        env_file = ".env"

print(DotSettings().dict())
# {'dot_env': 'dot_sample'}

.envを使用する場合は、サブクラスConfigenv_fileパラメータを付与します。
値に.envファイルパスを指定すれば設定完了です。

具体例

具体的な使用例です。

boto3.Session()のパラメータを環境変数で渡す想定です。

class ProfileSettings(BaseSettings):
    profile_name: str | None = Field(default=None, env="AWS_PROFILE_NAME")

    class Config:
        env_file = ".env"
        case_sensitive = True

# None以外の項目をアンパックして渡す
session = boto3.Session(**ProfileSettings().dict(exclude_none=True))

.envAWS_PROFILE_NAMEから値を読み込みprofile_nameに値を挿入。
セッション作成時にパラメータをアンパックして渡します。

dictメソッド内でexclude_none=Trueを設定しています。
これはインスタンス変数にNoneが指定されているパラメータを除外するオプションです。

開発環境ではプロファイル名を使用して、本番環境では使用しない。
といった制御を環境変数だけで実現できます。

まとめ

pydantic BaseSettingsで環境変数の取り扱いが楽になります。
今回は記述していませんが、BaseSettings自体はクラスなのでメソッド定義も可能です。
関係ロジックを一纏めにできるメリットもあります。

是非使ってみてください!

参考文献

Discussion