Python でプログラムを書いていると、設定に基づいて動作を変更したくなることがある。 そして、動作を変更する設定の値は、たとえば環境変数や設定ファイルから読み込みたくなる。 ただ、そういった処理を自分で書くと手間がかかるし不具合も作り込みやすい。 そのため、できるだけ専用のライブラリを使いたい。
今回は、そうした場面で役に立つライブラリとして Pydantic Settings を紹介する。 名前から分かるとおり Pydantic 系のライブラリのひとつ。
使った環境は次のとおり。
$ sw_vers ProductName: macOS ProductVersion: 26.3 BuildVersion: 25D125 $ uv --version uv 0.10.2 (Homebrew 2026-02-10)
もくじ
- もくじ
- 下準備
- 基本的な使い方
- バリデーション
- 変数名にプレフィックスをつける
- 設定をネストする
- 設定ファイル (dotenv) を読み込む
- 設定ファイルをホームディレクトリ配下から読み込む
- 設定ファイル (TOML) を読み込む
- 設定ファイル (シークレット) を読み込む
- CLI から設定を読み込む
- まとめ
下準備
以降で紹介するサンプルコードは PEP 723 1 形式で依存ライブラリなどの情報を記載している。 uv 経由で実行するため、あらかじめ uv をインストールしておく。
$ brew install uv
基本的な使い方
早速だけど以下にサンプルコードを示す。
Pydantic Settings では、まず pydantic_settings.BaseSettings を継承したクラスを定義する。
そして、その継承したクラスに設定の項目などを追加していく。
サンプルコードでは Settings という名前でクラスを定義して foo と bar という項目を追加している。
実際に使用するときはクラスをインスタンス化してメンバ変数を参照する。
# /// script # requires-python = ">=3.12" # dependencies = [ # "pydantic-settings>=2", # ] # /// from pydantic import Field from pydantic_settings import BaseSettings class Settings(BaseSettings): """BaseSettings を検証したクラスが設定の入れ物になる""" # クラス変数として値を定義していく foo: str = Field(default="hoge") bar: int = Field(default=42) def main(): # 定義したクラスをインスタンス化して使用する settings: Settings = Settings() # 設定された値は model_dump() メソッドで確認できる print(settings.model_dump()) # 個別にアクセスするときはメンバ変数として参照できる print(settings.foo) if __name__ == "__main__": main()
上記に適当な名前をつけて uv で実行する。
$ uv run --script example.py {'foo': 'hoge', 'bar': 42} hoge
どうやら、ちゃんと設定された内容を参照できている。
設定の中で特定の項目の値を変更したいときは、同名のシェル変数や環境変数を定義することで上書きできる。
以下はたとえばシェル変数を使った場合の例になる。
FOO というシェル変数に fuga を指定することで foo の値を上書きしている。
$ FOO=fuga uv run --script example.py {'foo': 'fuga', 'bar': 42}
同じように環境変数を使っても項目の値を上書きできる。
以下では環境変数の BAR を定義することで、bar の値を -1 に上書きしている。
$ export BAR=-1 $ uv run --script example.py {'foo': 'hoge', 'bar': -1}
後続の実行に影響を与えないように定義した環境変数は消しておく。
$ unset BAR
バリデーション
入力された内容は自動的にバリデーションされる。
試しに先ほどの内容で foo のデフォルト値を消してみよう。
こうすると foo は入力が必須の項目になる。
# /// script # requires-python = ">=3.12" # dependencies = [ # "pydantic-settings>=2", # ] # /// from pydantic import Field from pydantic_settings import BaseSettings class Settings(BaseSettings): # デフォルト値がないものは必須になる foo: str = Field() bar: int = Field(default=42) def main(): settings: Settings = Settings() print(settings.model_dump()) print(settings.foo) if __name__ == "__main__": main()
この状態で値を与えずに実行すると、次のように例外になる。 例外がスローされるのはクラスをインスタンス化するタイミング。 そしてスローされる例外は Pydantic の ValidationError だ。
$ uv run --script example.py Traceback (most recent call last): File "/Users/amedama/Documents/example.py", line 25, in <module> main() ~~~~^^ File "/Users/amedama/Documents/example.py", line 19, in main settings: Settings = Settings() ~~~~~~~~^^ File "/Users/amedama/.cache/uv/environments-v2/example-099949597fdfdd24/lib/python3.14/site-packages/pydantic_settings/main.py", line 242, in __init__ super().__init__(**__pydantic_self__.__class__._settings_build_values(sources, init_kwargs)) ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/amedama/.cache/uv/environments-v2/example-099949597fdfdd24/lib/python3.14/site-packages/pydantic/main.py", line 250, in __init__ validated_self = self.__pydantic_validator__.validate_python(data, self_instance=self) pydantic_core._pydantic_core.ValidationError: 1 validation error for Settings foo Field required [type=missing, input_value={}, input_type=dict] For further information visit https://errors.pydantic.dev/2.12/v/missing
シェル変数などで値を与えることで例外にならず実行できるようになる。
$ FOO=fuga uv run --script example.py {'foo': 'fuga', 'bar': 42} fuga
同様に int 型で定義されている bar に文字列を入れようとすると例外になる。
$ FOO=fuga BAR=hello uv run --script example.py Traceback (most recent call last): File "/Users/amedama/Documents/example.py", line 25, in <module> main() ~~~~^^ File "/Users/amedama/Documents/example.py", line 19, in main settings: Settings = Settings() ~~~~~~~~^^ File "/Users/amedama/.cache/uv/environments-v2/example-099949597fdfdd24/lib/python3.14/site-packages/pydantic_settings/main.py", line 242, in __init__ super().__init__(**__pydantic_self__.__class__._settings_build_values(sources, init_kwargs)) ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/amedama/.cache/uv/environments-v2/example-099949597fdfdd24/lib/python3.14/site-packages/pydantic/main.py", line 250, in __init__ validated_self = self.__pydantic_validator__.validate_python(data, self_instance=self) pydantic_core._pydantic_core.ValidationError: 1 validation error for Settings bar Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='hello', input_type=str] For further information visit https://errors.pydantic.dev/2.12/v/int_parsing
ここらへんは Pydantic を使ったバリデーションの流儀がそのまま通用する。
変数名にプレフィックスをつける
前述したとおり、デフォルトでは設定のメンバ変数と同名のシェル変数や環境変数を使って値を上書きする。
ただ、これだと変数名が散らばって分かりにくい。
そのため、通常はプログラムに固有のプレフィックスをつけたくなるだろう。
そのときは model_config という名前で SettingsConfigDict のクラス変数を BaseSettings を継承したクラスに用意する。
そして、引数の env_prefix で変数のプレフィックスを指定すれば良い。
以下のサンプルコードでは引数のプレフィックスとして example_prefix を指定している。
# /// script # requires-python = ">=3.12" # dependencies = [ # "pydantic-settings>=2", # ] # /// from pydantic import Field from pydantic_settings import BaseSettings from pydantic_settings import SettingsConfigDict class Settings(BaseSettings): foo: str = Field(default="hoge") bar: int = Field(default=42) # 基本的な設定は model_config: SettingsConfigDict を使う model_config = SettingsConfigDict( # たとえば環境変数に特定のプレフィックスをつける env_prefix="example_prefix_", ) def main(): settings: Settings = Settings() print(settings.model_dump()) if __name__ == "__main__": import os # プレフィックスのついた環境変数を使って設定を上書きする os.environ["EXAMPLE_PREFIX_FOO"] = "fuga" main()
なお、上記では os.environ 経由で環境変数を指定している。
このように、クラスをインスタンス化する前であれば値が反映される。
実行すると、ちゃんと値が上書きされていることが確認できる。
$ uv run --script example.py {'foo': 'fuga', 'bar': 42}
このように Pydantic Settings では model_config: SettingsConfigDict を使うことで設定に関する設定を変更する。
設定をネストする
プログラムによっては設定の項目が多岐にわたる。
数多くの項目を BaseSettings の直下にフラットな構造で扱おうとすると、かなり煩雑になってくる。
そのときは設定のまとまりごとにクラスを分割してメンバ変数をネストさせると良い。
以下にサンプルコードを示す。
ネストさせる設定は pydantic.BaseModel を継承したクラスにする。
そのクラスを BaseSettings を継承したクラスのクラス変数として追加すれば良い。
サンプルコードでは Settings に nested という名前で追加している。
# /// script # requires-python = ">=3.12" # dependencies = [ # "pydantic-settings>=2", # ] # /// from pydantic import BaseModel from pydantic import Field from pydantic_settings import BaseSettings from pydantic_settings import SettingsConfigDict class NestedConfig(BaseModel): """設定にネストした階層構造を持たせるときは BaseModel を継承したクラスを定義する""" foo: str = Field(default="hoge") bar: int = Field(default=42) class Settings(BaseSettings): # 設定をネストさせるときは BaseSettings のクラス変数として持たせる nested: NestedConfig model_config = SettingsConfigDict( env_prefix="example_prefix_", # 設定をネストさせるときは env_nested_delimiter を指定すると良い env_nested_delimiter="__", ) def main(): settings: Settings = Settings() print(settings.model_dump()) if __name__ == "__main__": import os # env_nested_delimiter で指定したデリミタを使ってネストした構造を表現できるようになる os.environ["EXAMPLE_PREFIX_NESTED__FOO"] = "fuga" main()
設定を環境変数で変更する場合には model_config で env_nested_delimiter も同時に指定しておくと良い。
この指定があると、デリミタでネストした構造の項目を指定できるようになる。
たとえばサンプルコードでは EXAMPLE_PREFIX_NESTED__FOO という変数名でネストしたメンバ変数の foo の値を変更している。
ちなみにデリミタとしてアンダースコアひとつを指定すると、アンダースコアを含む変数名が使えなくなるので注意すること。
実行すると、ちゃんとネストした foo が上書きされていることが分かる。
$ uv run --script example.py {'nested': {'foo': 'fuga', 'bar': 42}}
もうひとつのネストした項目を上書きする方法には JSON 文字列を使うやり方もある。
以下では EXAMPLE_PREFIX_NESTED という環境変数に JSON の文字列を渡している。
JSON 文字列が NestedConfig の構造を反映しているため各項目の値を上書きできる。
# /// script # requires-python = ">=3.12" # dependencies = [ # "pydantic-settings>=2", # ] # /// from pydantic import BaseModel from pydantic import Field from pydantic_settings import BaseSettings from pydantic_settings import SettingsConfigDict class NestedConfig(BaseModel): foo: str = Field(default="hoge") bar: int = Field(default=42) class Settings(BaseSettings): nested: NestedConfig model_config = SettingsConfigDict( env_prefix="example_prefix_", ) def main(): settings: Settings = Settings() print(settings.model_dump()) if __name__ == "__main__": import os # JSON 構造の文字列を指定することで設定を上書きする方法もある os.environ["EXAMPLE_PREFIX_NESTED"] = '{"foo": "fuga", "bar": 12345}' main()
実行すると、たしかに値が変更されている。
$ uv run --script example.py {'nested': {'foo': 'fuga', 'bar': 12345}}
設定ファイル (dotenv) を読み込む
ここまではシェル変数や環境変数をプロセスに直接渡すやり方だった。 とはいえ、項目が増えてくるとそれも煩雑なので設定ファイルに分離したくなる。 その場合の最初の選択肢は dotenv 形式のファイルになるだろう。 要するに環境変数をそのままプレーンテキストに書いたファイルを読み込む。
以下のサンプルコードではカレントディレクトリにある .env ファイルを読み込むようにしている。
といってもやることは model_config の引数に env_file や env_file_encoding を指定するだけ。
# /// script # requires-python = ">=3.12" # dependencies = [ # "pydantic-settings>=2", # ] # /// from pydantic import BaseModel from pydantic import Field from pydantic_settings import BaseSettings from pydantic_settings import SettingsConfigDict class NestedConfig(BaseModel): foo: str = Field(default="hoge") bar: int = Field(default=42) class Settings(BaseSettings): nested: NestedConfig model_config = SettingsConfigDict( env_prefix="example_prefix_", env_nested_delimiter="__", # dotenv 形式のファイルを読むようにするには env_file 引数でファイル名を指定する env_file=".env", env_file_encoding="utf-8", ) def main(): settings: Settings = Settings() print(settings.model_dump()) if __name__ == "__main__": # カレントディレクトリに .env ファイルを作る # NOTE: すでに存在するディレクトリで実行すると上書きされてしまう点に注意する dotenv_configs = [ "EXAMPLE_PREFIX_NESTED__FOO=fuga", "EXAMPLE_PREFIX_NESTED__BAR=12345", ] with open(".env", mode="w") as f: f.write("\n".join(dotenv_configs)) main()
上記ではプログラムの中で .env ファイルを作った上で Settings をインスタンス化することで設定を読ませている。
実行すると、ちゃんと設定が反映されていることが確認できる。
$ uv run --script example.py {'nest': {'foo': 'fuga', 'bar': 12345}}
作成される .env ファイルの中身は次のとおり。
$ cat .env EXAMPLE_PREFIX_NEST__FOO=fuga EXAMPLE_PREFIX_NEST__BAR=12345
なお、複数のファイル名に対応させたいときは env_file に渡す値を文字列が複数入ったイテレータにすれば良い。
また、BaseSettings をインスタンス化するタイミングで読み込むファイルを指定することもできる。
ただし、その場合は model_config で指定したファイルが読まれなくなる店に注意が必要になる。
以下では実際にインスタンス化するタイミングで .env.dev というファイルを読むように指定している。
# /// script # requires-python = ">=3.12" # dependencies = [ # "pydantic-settings>=2", # ] # /// from pydantic import BaseModel from pydantic import Field from pydantic_settings import BaseSettings from pydantic_settings import SettingsConfigDict class NestedConfig(BaseModel): foo: str = Field(default="hoge") bar: int = Field(default=42) class Settings(BaseSettings): nested: NestedConfig model_config = SettingsConfigDict( env_prefix="example_prefix_", env_nested_delimiter="__", env_file=( ".env", # 複数のファイル名に対応させたいときは env_file に渡す値をイテラブルにする ), env_file_encoding="utf-8", ) def main(): # インスタンス化するタイミングで読ませることもできる # ただし、model_config に指定したファイルは無視される点に注意する settings: Settings = Settings( _env_file=".env.dev", _env_file_encoding="utf-8", ) print(settings.model_dump()) if __name__ == "__main__": dotenv_configs = [ "EXAMPLE_PREFIX_NESTED__FOO=fuga", ] with open(".env", mode="w") as f: f.write("\n".join(dotenv_configs)) dotenv_dev_configs = [ "EXAMPLE_PREFIX_NESTED__BAR=12345", ] with open(".env.dev", mode="w") as f: f.write("\n".join(dotenv_dev_configs)) main()
実行すると .env の方は反映されず、.env.dev の方だけが反映されている。
$ uv run --script example.py {'nest': {'foo': 'hoge', 'bar': 12345}}
終わったら後片付けしておこう。
$ rm .env .env.dev
設定ファイルをホームディレクトリ配下から読み込む
実行する場所に依存しない形でホームディレクトリの配下に設定ファイルを置きたい場合もあるはず。
その場合はパスを pathlib.Path#expanduser() を使って指定すれば良い。
以下のサンプルコードではホームディレクトリの直下から .env.example というファイル名で読み込んでいる。
# /// script # requires-python = ">=3.12" # dependencies = [ # "pydantic-settings>=2", # ] # /// from pathlib import Path from pydantic import BaseModel from pydantic import Field from pydantic_settings import BaseSettings from pydantic_settings import SettingsConfigDict class NestedConfig(BaseModel): foo: str = Field(default="hoge") bar: int = Field(default=42) class Settings(BaseSettings): nested: NestedConfig model_config = SettingsConfigDict( env_prefix="example_prefix_", env_nested_delimiter="__", env_file=( # ホームディレクトリを扱いたい場合には pathlib.Path#expanduser() を使う Path("~/.env.example").expanduser(), ), env_file_encoding="utf-8", ) def main(): settings: Settings = Settings() print(settings.model_dump()) if __name__ == "__main__": main()
実際にファイルを用意してみよう。
$ cat << 'EOF' > ~/.env.example EXAMPLE_PREFIX_NESTED__FOO=fuga EXAMPLE_PREFIX_NESTED__BAR=12345 EOF
実行すると、たしかにファイルから値が読み込まれている。
$ uv run --script example.py {'nest': {'foo': 'fuga', 'bar': 12345}}
なお、実際にこのイディオムを使うときはホームディレクトリの .config 配下などに置くのが適切だろう。
設定ファイル (TOML) を読み込む
Python のプログラムが参照する設定ファイルとしては TOML 形式もポピュラーだろう。
しかし、TOML 形式のファイルは、デフォルトでは読み込み対象になっていない。
そのため BaseSettings クラスにクラスメソッドの settings_customise_sources を定義して動作をオーバーライドする必要がある。
以下のサンプルコードでは config.toml という名前で TOML 形式のファイルを読み込んでいる。
# /// script # requires-python = ">=3.12" # dependencies = [ # "pydantic-settings>=2", # ] # /// from pydantic import BaseModel from pydantic import Field from pydantic_settings import BaseSettings from pydantic_settings import PydanticBaseSettingsSource from pydantic_settings import SettingsConfigDict from pydantic_settings import TomlConfigSettingsSource class NestedConfig(BaseModel): bar: int = Field(default=42) class Settings(BaseSettings): nested: NestedConfig = NestedConfig() foo: str = Field(default="hoge") model_config = SettingsConfigDict( env_prefix="example_prefix_", env_nested_delimiter="__", # toml ファイルを読むときは toml_file を指定する toml_file="config.toml", ) @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: """設定を何処からどの順序で読み込むかを指定するフックポイント""" # デフォルトでは TOML を読み込む設定がないので追加する return ( # 以下の 4 つはデフォルトで反映される内容 init_settings, env_settings, dotenv_settings, file_secret_settings, # TOML の反映を最も低い優先度で追加している TomlConfigSettingsSource(settings_cls), ) def main(): settings: Settings = Settings() print(settings.model_dump()) if __name__ == "__main__": # config.toml ファイルを作る toml_configs = [ 'foo="fuga"', "", "[nested]", "bar=12345", "", ] with open("config.toml", mode="w") as f: f.write("\n".join(toml_configs)) main()
TOML 形式の設定ファイルは先ほどの dotenv 形式のファイルと同様に main() 関数を呼び出す前に作成している。
上記を実行すると、ちゃんと設定ファイルの内容が読まれることが分かる。
$ uv run --script example.py {'nested': {'bar': 12345}, 'foo': 'fuga'}
終わったら後片付けする。
$ rm config.toml
設定ファイル (シークレット) を読み込む
パスワードやトークンなど公にできない情報を、専用のファイルに書き込んでおいてそこから読み込んで使うようなケースもある。
Pydantic Settings ではシークレットと呼んでいる。
シークレットのファイルは形式としては dotenv に近いものの、中身が = で分割されたキーバリュー形式になっていない。
入っているのは単一のバリューのみで、キーはファイル名で表される。
以下のサンプルコードでは /tmp をシークレットを読み込むディレクトリに指定している。
もちろん、実際にはもっと別のパスを使うことになる。
また、ネストした項目に値を読み込む際にはファイル名をアンダースコア 2 つで区切って識別する指定が入っている。
# /// script # requires-python = ">=3.12" # dependencies = [ # "pydantic-settings>=2", # ] # /// from pydantic import BaseModel from pydantic import Field from pydantic import SecretStr from pydantic_settings import BaseSettings from pydantic_settings import NestedSecretsSettingsSource from pydantic_settings import SettingsConfigDict class NestedConfig(BaseModel): # シークレットを扱うときは型を Secret* にする secret_token: SecretStr = Field() class Settings(BaseSettings): nested: NestedConfig # シークレットを扱うときは型を Secret* にする secret_password: SecretStr = Field() model_config = SettingsConfigDict( # シークレットの書かれたファイルを特定のディレクトリから読み込む secrets_dir="/tmp", # ネストした要素に読み込む場合はいくつかのやり方がある # 単純にファイル名で階層を識別するなら secrets_nested_delimiter を使う secrets_nested_delimiter="__", ) @classmethod def settings_customise_sources( cls, settings_cls, init_settings, env_settings, dotenv_settings, file_secret_settings, ): return ( init_settings, env_settings, dotenv_settings, # ネストした項目に値を読み込む場合はソースを変更する必要がある # SecretsSettingsSource の代わりに NestedSecretsSettingsSource を使用する NestedSecretsSettingsSource(file_secret_settings), ) def main(): settings: Settings = Settings() # Secret* はそのままだとマスクして表示される print(settings.model_dump()) # 使うときは get_secret_value() メソッドで参照する print(settings.secret_password.get_secret_value()) print(settings.nested.secret_token.get_secret_value()) if __name__ == "__main__": main()
試しにダミーのシークレットを /tmp 以下に用意しよう。
$ cat << 'EOF' >> /tmp/secret_password super-secret-password EOF $ cat << 'EOF' >> /tmp/nested__secret_token super-secret-token EOF
実行すると、ちゃんとエラーにならずシークレットのファイルが読まれる。
$ uv run --script example.py {'nested': {'secret_token': SecretStr('**********')}, 'secret_password': SecretStr('**********')} super-secret-password super-secret-token
読み込む先のメンバ変数の型を SecretStr にしているので model_dump() で表示される内容はアスタリスクでマスクされている。
実際に取り出して使う際には get_secret_value() から参照する。
もちろん、実際にはサンプルコードのように標準出力やログに出力してしまうのはまずい。
終わったら後片付けしておこう。
$ rm /tmp/secret_password /tmp/nested__secret_token
CLI から設定を読み込む
また、環境変数や設定ファイルだけでなく CLI から設定を読み込めるようにするオプションもある。
この機能を使うときは model_config の引数 cli_parse_args に True を指定する。
# /// script # requires-python = ">=3.12" # dependencies = [ # "pydantic-settings>=2", # ] # /// from pydantic import BaseModel from pydantic import Field from pydantic_settings import BaseSettings from pydantic_settings import SettingsConfigDict class NestedConfig(BaseModel): bar: int = Field(default=42) class Settings(BaseSettings): nested: NestedConfig foo: str = Field(default="hoge") model_config = SettingsConfigDict( # CLI から設定を読めるようにするには cli_parse_args を有効にする cli_parse_args=True, ) def main(): settings: Settings = Settings() print(settings.model_dump()) if __name__ == "__main__": main()
これだけでプログラムが CLI の引数を受け取るスクリプトになる。
たとえば --help 引数を受け取って自動的に usage を出力できる。
$ uv run --script example.py --help usage: example.py [-h] [--nested [JSON]] [--nested.bar int] [--foo str] options: -h, --help show this help message and exit --foo str (default: hoge) nested options: --nested [JSON] set nested from JSON string (default: {}) --nested.bar int (default: 42)
usage の内容から指定していくと、ちゃんと動作に反映されることが確認できる。
$ uv run --script example.py --foo fuga --nested.bar -1 {'nested': {'bar': -1}, 'foo': 'fuga'}
まとめ
今回は Pydantic Settings を使って Python のプログラムの設定を扱う方法について紹介した。 特に気に入っているのは、小さくシンプルに始めることができて、なおかつ細かいニーズにも柔軟に対応できるところ。 この記事で使っている設定は全体の一部で、実際にはもっと色々なカスタマイズもできる。
