開発

pydantic-settingsの使い方:BaseSettingsで.env・環境変数・シークレットを型安全に読む

pydantic-settingsの使い方:BaseSettingsで.env・環境変数・シークレットを型安全に読む

pydantic-settingsは、Pydanticのモデルと同じ書き方で設定クラスを定義し、環境変数と.envファイルから値を読み込んで型を検証するライブラリです。BaseSettingsを継承したクラスにport: intと書けば、環境変数の文字列"8000"は整数に変換され、"abc"なら起動の時点でValidationErrorになります。この記事でコードを示す範囲は、最新版2.16.3(2026年11月時点)でDB接続情報とAPIキーを持つ設定クラスを作り、FastAPIへDependsで注入するまでの手順です。共用の.envで起きる「Extra inputs are not permitted」の対処や、SecretStrでパスワードをログに出さない書き方も扱います。

まとめ:pydantic-settingsで設定を読むときの結論

設定値が5つを超え、環境ごとに値が変わるアプリなら、os.environを散らさずBaseSettingsのクラス1つへ集めます。型と必須項目が起動時に検証され、環境変数の付け忘れをデプロイの段階で止められます。

読み込みの優先順位は「初期化引数>環境変数>.env>secretsのファイル>既定値」です。.envを他のツールと共用するならenv_prefixを付け、dotenv_filtering="match_prefix"で自分の変数だけを読ませます。パスワードとAPIキーはSecretStrで受け、本番ではファイルかクラウドの保管庫から渡します。

FastAPIでは設定クラスをDependsで注入し、settings_cached()かlru_cacheで読み込みを1回に絞ります。

pydantic-settings 2.16の導入と環境変数を読む最小コード

まずはインストールと、環境変数を1つ読むだけの最小の設定クラスで動きを確かめます。

pydantic-settingsの導入とPython 3.11以上の動作要件

PyPIのpydantic-settingsでは、2026年11月14日時点の最新が2.16.3(2026年11月13日公開)です。2.16.0からPython 3.11以上が必要になり、3.10の環境では2.15系までしか入りません。依存としてpydantic2.7.0以上とpython-dotenv1.2.2以上が一緒に入るため、.envを読むために別のパッケージを足す必要はありません。

# 仮想環境を作り、pydantic-settings を入れる
python3 -m venv .venv
source .venv/bin/activate
pip install "pydantic-settings==2.16.3"

# 入った版を確かめる
python -c "import pydantic_settings; print(pydantic_settings.__version__)"

pydantic本体のv1時代はfrom pydantic import BaseSettingsで使えましたが、v2で別パッケージに分かれました。古い記事のコードをそのまま貼るとPydanticImportErrorになります。Pydanticそのものの位置付けとv1との違いは、Pydanticの読み方・BaseModelの使い方とV1との違いで整理しています。

BaseSettingsを継承した設定クラスと型変換・必須項目の検証

次のファイルをconfig.pyとして置きます。

# config.py
from pydantic_settings import BaseSettings


class Settings(BaseSettings):
    app_name: str = "sample-api"
    port: int = 8000
    debug: bool = False
    admin_email: str  # 既定値なし=必須


settings = Settings()
print(settings.model_dump())
# 必須の admin_email が無いと起動時に止まる
python config.py
# pydantic_core._pydantic_core.ValidationError: 1 validation error for Settings
# admin_email
#   Field required ...

# 環境変数で渡すと、文字列が型に合わせて変換される
[email protected] PORT=9000 DEBUG=true python config.py
# {'app_name': 'sample-api', 'port': 9000, 'debug': True, 'admin_email': '[email protected]'}

環境変数名はフィールド名と同じで、既定では大文字と小文字を区別しません。PORT=abcと渡せばintに変換できず、ValidationErrorで止まる仕組みです。既定値も検証されるため、既定値の型の誤りも起動時に見つかります。

.envと環境変数を読む優先順位と複数ファイル・共用.envの扱い方

同じ項目が複数の場所に書かれていたとき、どれが勝つかを先に押さえておくと、「設定したのに反映されない」の大半は自分で切り分けられます。

初期化引数・環境変数・.env・secrets・既定値の優先順位

公式のSettings Managementが定める優先順位は、上ほど強い次の6段です。

  1. CLI引数(cli_parse_argsを有効にしたときだけ)
  2. Settings(port=9000)のような初期化引数
  3. 環境変数
  4. .envファイルの値
  5. secrets_dirに置いたファイルの値
  6. クラスに書いた既定値

実務で効いてくるのは3と4の関係です。シェルにexport DEBUG=falseが残っていると、.envにDEBUG=trueと書いてもfalseのまま動きます。.envを書き換えても反映されないときは、まずenv | grep -i debugで同名の環境変数が無いかを見ます。

env_fileに.envと.env.prodを渡したときの後勝ちの仕組み

.envを読むにはmodel_configにenv_fileを書きます。タプルで複数渡すと前から順に読み、同じキーは後のファイルの値で上書きされる仕組みです。

from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        # 共通の .env を読んだあと、手元だけの .env.local で上書きする
        env_file=(".env", ".env.local"),
        env_file_encoding="utf-8",
    )

    port: int = 8000
    debug: bool = False

無いファイルはエラーにならず読み飛ばされるため、.env.localは開発者の手元にだけ置けば済みます。本番で.envを読ませたくない場合は、Settings(_env_file=None)のように初期化時にNoneを渡します。相対パスは起動したディレクトリから探すため、2.16.0では親ディレクトリまでさかのぼるenv_file_depth(既定は0)が加わりました。.envそのものの記法や、Gitに載せない運用はdotenvの書き方と読み込み方の解説に譲ります。

共用の.envで発生する余分な入力エラーの原因と設定による対処

Docker Composeと同じ.envを使っていると、次のエラーで起動しないことがあります。

pydantic_core._pydantic_core.ValidationError: 1 validation error for Settings
postgres_password
  Extra inputs are not permitted [type=extra_forbidden, input_value='local-pass', input_type=str]

原因は2つの既定値の組み合わせです。BaseSettingsのextraは"forbid"で、2.16.3のソースでは、.envのエントリはフィールドに当たらないものも含めて全部モデルに渡されます。Compose用のPOSTGRES_PASSWORDがフィールドに無いため、余計な入力として弾かれる仕組みです。環境変数の側はフィールドに当たらないものを無視するので、同じ値をexportしたときは起きません。

対処は3通りあり、おすすめは上から順です。

設定 .envの余計なキー 自分の変数の打ち間違い
env_prefix+match_prefix 無視する エラーで気付ける
only_existing指定 無視する 黙って無視される
extra=”ignore” 無視する 黙って無視される

env_prefix="APP_"とdotenv_filtering="match_prefix"を組み合わせると、APP_で始まるキーだけがモデルに渡ります。APP_DEBGU=trueのような打ち間違いは余計な入力として止まり、POSTGRES_PASSWORDは無視されます。extra="ignore"は手軽ですが、打ち間違えた変数名が既定値のまま黙って動くため、本番でdebugが効いていないことに気付けません。extraの各値の挙動はConfigDictの設定一覧で比較しています。

env_prefixとenv_nested_delimiterによるネスト設定

設定が増えてくると、DBやメールなどの単位でまとめたくなります。ここではアプリ全体の設定クラスを、記事の後半まで使う形で組み立てます。

APP_のprefixとDATABASE__HOSTでサブモデルへ値を入れる設定

# config.py
from pydantic import BaseModel, SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict


class DatabaseSettings(BaseModel):
    host: str = "localhost"
    port: int = 5432
    name: str = "app"
    user: str = "app"
    password: SecretStr


class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        env_prefix="APP_",
        env_nested_delimiter="__",
        env_file=(".env", ".env.local"),
        env_file_encoding="utf-8",
        dotenv_filtering="match_prefix",
    )

    debug: bool = False
    allowed_origins: list[str] = ["http://localhost:3000"]
    database: DatabaseSettings
    openai_api_key: SecretStr
# .env(Docker Compose と共用。APP_ で始まらない行は読まれない)
POSTGRES_PASSWORD=local-pass
APP_DEBUG=true
APP_ALLOWED_ORIGINS=["https://example.com","https://admin.example.com"]
APP_DATABASE__HOST=db
APP_DATABASE__PASSWORD=local-pass
APP_OPENAI_API_KEY=sk-local-dummy

env_nested_delimiter="__"を指定すると、APP_DATABASE__HOSTはdatabase.hostに入ります。区切りを1文字の_にすると、openai_api_keyのようにフィールド名自体に含まれる_まで分割されてしまうため、2文字の__にするのが定石です。

env_prefixは既定でフィールド名から作る変数名にだけ付き、validation_aliasで別名を付けたフィールドには付きません。別名にも付けたいときはenv_prefix_target="all"を指定します。

alias・validation_alias・serialization_alias の3種の違いは「Pydantic Fieldの使い方:default_factory・alias・制約の引数一覧とOpenAPIへの反映」で解説しています。

listとdictはJSON文字列で渡すルールと空文字の扱いの違い

上のAPP_ALLOWED_ORIGINSのように、list・dict・サブモデルの型を持つフィールドは、環境変数の値をJSONとして解釈します。APP_ALLOWED_ORIGINS=https://example.com,https://admin.example.comとカンマ区切りで書くとJSONとして読めず、SettingsErrorで起動に失敗します。カンマ区切りで渡したい事情があるなら、フィールドをstrで受けてfield_validatorで分割する方が素直です。

空文字の扱いも、設定を読み込む際の落とし穴です。APP_PORT=のように値が空の行は、既定(env_ignore_empty=False)では空文字として渡され、intのフィールドならValidationErrorになります。テンプレートの.env.exampleを値を空のまま複製する運用なら、env_ignore_empty=Trueにして空の行を「未設定」として扱わせます。

SecretStrとsecrets_dirでパスワード・APIキーをログに出さない実装

設定クラスをそのままprintやログに出すと、パスワードまで平文で残ります。ここでは表示を伏せる型と、ファイルから渡す仕組みを組み合わせます。

SecretStrで表示を伏せget_secret_valueで取り出す使い方

先ほどの設定クラスでpasswordとopenai_api_keyをSecretStrにしたのは、このためです。PydanticのTypesのAPIに記載のとおり、SecretStrはstr()とrepr()で値を**********に置き換えます。

from config import Settings

settings = Settings()
print(settings.openai_api_key)
# **********
print(settings.database)
# host='db' port=5432 name='app' user='app' password=SecretStr('**********')

# 実際に使う場所でだけ元の値を取り出す
api_key = settings.openai_api_key.get_secret_value()

伏せられるのは表示とJSON出力までで、get_secret_value()で取り出した後は普通のstrに戻ります。取り出すのはSDKのクライアントを作る1か所に限り、変数に入れて持ち回らないようにします。

Docker secretsのファイルをsecrets_dirで読み込む設定と注意

本番では、パスワードを環境変数に置かずファイルで渡す構成もよく使います。環境変数はdocker inspectや子プロセスから見えるためです。Docker Composeのsecretsは、指定した値をコンテナ内の/run/secrets/以下にファイルとして置きます。

# compose.yaml
services:
  api:
    build: .
    secrets:
      - app_openai_api_key
secrets:
  app_openai_api_key:
    file: ./secrets/openai_api_key.txt

設定クラスのmodel_configにsecrets_dir="/run/secrets"を足すと、ファイル名をキー、中身を値として読みます。ファイル名にはenv_prefixが付くため、上の例で使うファイル名はapp_openai_api_keyです。ディレクトリが無い環境では警告が出るだけで止まらないため、開発機と本番で同じクラスを使えます。

2.16.3(2026年11月13日)では、secrets_dirの外を指すシンボリックリンクを追わないよう修正されました(v2.16.3のリリースノート)。ネストしたsecretsを読むNestedSecretsSettingsSourceは2.14.2で同種の問題が直っています。secrets_dirを使うなら2.16.3以上に上げておきます。

AWS Secrets Managerなどクラウドの保管庫から読む拡張の選び方

pydantic-settingsには、クラウドの保管庫から値を読むソースも同梱されています。使うには、PyPIに登録されたextraを付けてインストールします。次の表のGCPはGoogle Cloudの略で、対象の保管庫はGoogle Cloud Secret Managerです。

保管庫 インストールするextra
AWS Secrets Manager aws-secrets-manager
AWS SSM Parameter Store aws-systems-manager
Azure Key Vault azure-key-vault
GCP Secret Manager gcp-secret-manager

これらは既定の読み込み順に入っていないため、settings_customise_sourcesを上書きし、返すタプルに追加します(先頭ほど優先)。起動時に1回読んだ値は変わらないので、保管庫側でパスワードを自動ローテーションするなら再起動を伴う運用にします。料金とローテーションの設定はAWS Secrets Managerの料金とローテーション設定で扱っています。

FastAPIで設定クラスをDependsから注入しテストで差し替える構成

FastAPI公式のSettings and Environment Variablesは、設定をモジュールの変数に置かず、依存関数から渡す形を勧めています。FastAPI 0.162.7のfastapi[standard]にはpydantic-settingsも含まれますが、指定は2.0.0以上と緩いため、ここまでの機能を使うならpydantic-settings>=2.16.3を別に固定します。

lru_cacheとsettings_cachedで設定の読み込みを1回にする書き方

Settings()は呼ぶたびに環境変数と.env、secretsのファイルを読み直して検証します。リクエストごとに作るとディスクの読み込みが毎回走るため、1回だけ作って使い回します。公式の例は@lru_cacheを付けた関数ですが、2.16.0で同じ役割のsettings_cached()がBaseSettingsに加わりました(v2.16.0のリリースノート)。

# main.py
from typing import Annotated

from fastapi import Depends, FastAPI

from config import Settings

app = FastAPI()


def get_settings() -> Settings:
    # 初回だけ読み込み、以降は同じインスタンスを返す(2.16.0以上)
    return Settings.settings_cached()


SettingsDep = Annotated[Settings, Depends(get_settings)]


@app.get("/info")
def info(settings: SettingsDep):
    return {"debug": settings.debug, "db_host": settings.database.host}

settings_cached()はクラスごとにキャッシュを持ち、settings_clear_cache()を呼ぶと次の呼び出しで読み直します。2.15系以前の環境で同じ動きにする方法は、get_settingsに@lru_cacheを付けてreturn Settings()と書く形です。Dependsそのものの仕組みと、Annotatedで型エイリアスにする書き方はFastAPIのDependsの使い方で詳しく扱っています。

キャッシュはワーカーのプロセスごとに1つです。--workers 4なら4回読み込まれ、どこかで値を書き換えても他のワーカーには伝わりません。DBの接続プールのように、設定から作ってアプリ全体で共有する資源はFastAPIのlifespanでの起動処理で作ります。

dependency_overridesでテスト用の設定へ差し替える手順

エンドポイントがget_settings経由で設定を受け取っていれば、テストでは依存関数ごと差し替えられます。

# test_main.py
from fastapi.testclient import TestClient

from config import DatabaseSettings, Settings
from main import app, get_settings


def get_test_settings() -> Settings:
    # .env を読まず、テストに必要な値だけを初期化引数で渡す
    return Settings(
        _env_file=None,
        debug=True,
        database=DatabaseSettings(host="test-db", password="test"),
        openai_api_key="sk-test",
    )


def test_info():
    app.dependency_overrides[get_settings] = get_test_settings
    try:
        client = TestClient(app)
        res = client.get("/info")
        assert res.json() == {"debug": True, "db_host": "test-db"}
    finally:
        app.dependency_overrides.clear()

_env_file=Noneは、手元の.envでテスト結果が変わるのを防ぐためです。初期化引数は環境変数より優先されるので、CIのランナーにAPP_DEBUGが残っていても、テストで渡した値が勝ちます。

pydantic-settingsを採用する条件と使わずに済ませる場面の判断

便利なライブラリですが、すべての設定読み込みに入れる必要はありません。判断の軸は「設定の数」と「型の誤りが起きたときの損害」です。

pydantic-settingsを使わずos.environで足りる場面と移行の目安

読む値が1〜2個の使い捨てスクリプトや、AWS Lambdaの小さな関数なら、os.environ["TABLE_NAME"]で十分です。依存の追加と、2.16.0のようなPython対応版の引き上げに追従する手間の方が重くなります。

次のどれかに当てはまったら、BaseSettingsへ移す時期です。

  • 設定値が5つを超え、intやboolへの変換をあちこちで手書きしている
  • 開発・ステージング・本番で.envや環境変数を切り替えている
  • APIキーやDBのパスワードを扱い、ログへの漏れを防ぎたい
  • FastAPIで、テストのたびに設定を差し替えたい

最優先は1つ目です。os.environ.get("DEBUG") == "true"のような比較が3か所に散ると、どこか1か所だけ"True"や"1"を受け付けない、という不一致が必ず起きます。既存のFlaskやDjangoのAPIをFastAPIへ移しながら、設定の持ち方やシークレットの渡し方も整理したい場合は、API開発・システム連携の受託開発でご相談を受け付けています。

実行中の設定変更やフロントエンドとの設定共有で失敗するパターン

pydantic-settingsが向かないのは、実行中に設定を変えたい場面です。機能フラグのように運用中に切り替える値はDBや機能フラグのサービスに置き、pydantic-settingsにはその接続先だけを持たせます。

もう1つは、フロントエンドや他言語のサービスと同じ設定ファイルを共有する場面です。env_nested_delimiterの__やJSONで書くlistは、Node.jsのdotenvにとってはただの文字列です。共有するのは接続先のURLのような単純な値に留め、型付きの構造はPython側の設定クラスで組み立てます。言語をまたいだ環境変数の受け渡しは環境変数の設定方法とDocker・CIでの受け渡しで整理しています。

よくある質問

pydantic-settingsの導入時に出てくることの多い質問に、2026年11月時点の版(pydantic-settings 2.16.3、pydantic 2.14.3)で答えます。

from pydantic import BaseSettings でエラーになるのはなぜですか?

Pydantic v2でBaseSettingsが本体から外れ、pydantic-settingsという別パッケージに移ったためです。pydantic 2.14.3で古い書き方をすると、PydanticImportErrorで「BaseSettings has been moved to the pydantic-settings package」と表示されます。インストールにはpip install pydantic-settingsを使い、インポート文の変更先はfrom pydantic_settings import BaseSettingsです。v1のclass Configで書いた設定は、model_config = SettingsConfigDict(...)に移します。

環境変数名の大文字と小文字は区別されますか?

既定(case_sensitive=False)では区別しません。PORTでもportでも同じフィールドに入ります。2.15.0からは、この設定が初期化引数と設定ファイル(JSON・TOML・YAML)にも効くようになりました。区別したい場合はcase_sensitive=Trueにしますが、Windowsの環境変数には効きません。

pydantic-settingsとpython-dotenvはどう使い分けますか?

python-dotenvは.envの中身を環境変数へ流し込むだけで、型の変換も必須項目の確認もしません。pydantic-settingsは内部でpython-dotenvを使って.envを読み、その上で型を検証します。Pythonのアプリなら、load_dotenv()を呼ぶ代わりにenv_fileを指定すれば足り、両方を呼ぶ必要はありません。

.envを書き換えたら実行中のアプリに反映されますか?

反映されません。設定クラスのインスタンスが持ち続けるのは、そのインスタンスを作った時点で読み込んだ値です。settings_cached()を使っているならSettings.settings_clear_cache()を呼んだ後の呼び出しで読み直しますが、ワーカーが複数あれば各プロセスで呼ぶ必要があります。開発中はfastapi devや--reloadの自動再起動に任せ、本番では値の変更をデプロイか再起動とセットにします。

既定値にも型のバリデーションは掛かりますか?

はい、既定値も型のバリデーションの対象です。BaseModelと違い、BaseSettingsはvalidate_default=Trueが既定で、port: int = "abc"のような誤りは起動時にエラーになります。止めたい場合は、model_configにvalidate_default=Falseを書くか、フィールドごとにField(validate_default=False)を指定します。

関連記事

お気に入りに入れた記事の一覧

一創の公式 SNS

X では毎朝の新着記事と IT ニュースの速報を、YouTube・TikTok・Instagram・LinkedIn・Facebook では記事の解説動画を配信しています。

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.11.11 テックブログ Microsoft 2026年11月の月例更新:Outlookの悪用済みCVE-2026-124194と修正版の確認方法
  2. 2026.11.13 テックブログ 宝くじ公式サイト不正アクセス:4,436名の対象確認とパスワード再設定、変更された仕組み
  3. 2026.11.13 テックブログ ignica不正アクセス:6万4,003件の氏名とメールアドレス流出の可能性と対象確認
  4. 2026.11.13 テックブログ KARTEの不正アクセス:全機能停止で利用企業がタグ・配信・データ連携を確かめる手順
  5. 2026.11.11 テックブログ JANISの不正アクセス:最大11,652名の医療機関情報と情報窃取プログラムの検知設定

RELATED POSTS 関連記事

目次