Python でプログラムを書いていると、設定に基づいて動作を変更したくなることがある。
そして、動作を変更する設定の値は、たとえば環境変数や設定ファイルから読み込みたくなる。
ただ、そういった処理を自分で書くと手間がかかるし不具合も作り込みやすい。
そのため、できるだけ専用のライブラリを使いたい。
今回は、そうした場面で役に立つライブラリとして Pydantic Settings を紹介する。
名前から分かるとおり Pydantic 系のライブラリのひとつ。
docs.pydantic.dev
使った環境は次のとおり。
$ sw_vers
ProductName: macOS
ProductVersion: 26.3
BuildVersion: 25D125
$ uv --version
uv 0.10.2 (Homebrew 2026-02-10)
もくじ
下準備
以降で紹介するサンプルコードは PEP 723 1 形式で依存ライブラリなどの情報を記載している。
uv 経由で実行するため、あらかじめ uv をインストールしておく。
$ brew install uv
基本的な使い方
早速だけど以下にサンプルコードを示す。
Pydantic Settings では、まず pydantic_settings.BaseSettings を継承したクラスを定義する。
そして、その継承したクラスに設定の項目などを追加していく。
サンプルコードでは Settings という名前でクラスを定義して foo と bar という項目を追加している。
実際に使用するときはクラスをインスタンス化してメンバ変数を参照する。
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()
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 は入力が必須の項目になる。
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 を指定している。
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(
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 という名前で追加している。
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):
nested: NestedConfig
model_config = SettingsConfigDict(
env_prefix="example_prefix_",
env_nested_delimiter="__",
)
def main():
settings: Settings = Settings()
print(settings.model_dump())
if __name__ == "__main__":
import os
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 の構造を反映しているため各項目の値を上書きできる。
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
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 を指定するだけ。
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_encoding="utf-8",
)
def main():
settings: Settings = Settings()
print(settings.model_dump())
if __name__ == "__main__":
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 というファイルを読むように指定している。
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_encoding="utf-8",
)
def main():
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 というファイル名で読み込んでいる。
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=(
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 形式のファイルを読み込んでいる。
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_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, ...]:
"""設定を何処からどの順序で読み込むかを指定するフックポイント"""
return (
init_settings,
env_settings,
dotenv_settings,
file_secret_settings,
TomlConfigSettingsSource(settings_cls),
)
def main():
settings: Settings = Settings()
print(settings.model_dump())
if __name__ == "__main__":
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 つで区切って識別する指定が入っている。
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_token: SecretStr = Field()
class Settings(BaseSettings):
nested: NestedConfig
secret_password: SecretStr = Field()
model_config = SettingsConfigDict(
secrets_dir="/tmp",
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,
NestedSecretsSettingsSource(file_secret_settings),
)
def main():
settings: Settings = Settings()
print(settings.model_dump())
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 を指定する。
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_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 のプログラムの設定を扱う方法について紹介した。
特に気に入っているのは、小さくシンプルに始めることができて、なおかつ細かいニーズにも柔軟に対応できるところ。
この記事で使っている設定は全体の一部で、実際にはもっと色々なカスタマイズもできる。