コンテンツにスキップ

設定と環境変数

🌐 AI と人間による翻訳

この翻訳は、人間のガイドに基づいて AI によって作成されました。🤝

原文の意図を取り違えていたり、不自然な表現になっている可能性があります。🤖

AI LLM をより適切に誘導するのを手伝う ことで、この翻訳を改善できます。

英語版

多くの場合、アプリケーションは外部の設定や構成を必要とします。たとえば、シークレットキー、データベース認証情報、メールサービスの認証情報などです。

これらの設定の多くは可変(変更されうる)で、データベースのURLのようなものがあります。また、多くはシークレットのように機微な情報です。

そのため、アプリケーションが読み取る環境変数で提供するのが一般的です。

豆知識

環境変数について理解するには、環境変数を参照してください。

型とバリデーション

これらの環境変数は Python の外部にあり、他のプログラムやシステム全体(Linux、Windows、macOS といった異なるOSを含む)と互換性が必要なため、文字列テキストのみを扱えます。

つまり、Python で環境変数から読み取られる値はすべて str になり、他の型への変換やバリデーションはコードで行う必要があります。

Pydantic の Settings

幸いなことに、Pydantic には環境変数から来る設定を扱うための優れたユーティリティがあり、Pydantic: Settings management で提供されています。

pydantic-settings のインストール

まず、仮想環境を作成して有効化し、pydantic-settings パッケージをインストールします:

$ pip install pydantic-settings
---> 100%

また、次のように all エクストラをインストールすると付属します:

$ pip install "fastapi[all]"
---> 100%

Settings オブジェクトを作成

Pydantic から BaseSettings をインポートして、そのサブクラスを作成します。これは Pydantic モデルとほぼ同じです。

Pydantic モデルと同様に、型アノテーションと(必要なら)デフォルト値を持つクラス属性を宣言します。

Field() による追加バリデーションなど、Pydantic モデルで使えるのと同じバリデーション機能をすべて利用できます。

from fastapi import FastAPI
from pydantic_settings import BaseSettings


class Settings(BaseSettings):
    app_name: str = "Awesome API"
    admin_email: str
    items_per_user: int = 50


settings = Settings()
app = FastAPI()


@app.get("/info")
async def info():
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }

豆知識

コピペ用に手早く使いたい場合は、この例ではなく、下の最後の例を使ってください。

その後、その Settings クラスのインスタンス(この例では settings オブジェクト)を作成すると、Pydantic は環境変数を大文字小文字を区別せずに読み取ります。つまり、大文字の APP_NAME という変数は、属性 app_name に対しても読み取られます。

次に、データが変換・バリデーションされます。したがって、settings オブジェクトを使うと、宣言した型のデータ(例: items_per_userint)が得られます。

settings の使用

次に、アプリケーションで新しい settings オブジェクトを使用できます:

from fastapi import FastAPI
from pydantic_settings import BaseSettings


class Settings(BaseSettings):
    app_name: str = "Awesome API"
    admin_email: str
    items_per_user: int = 50


settings = Settings()
app = FastAPI()


@app.get("/info")
async def info():
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }

サーバーを実行

次に、設定を環境変数として渡してサーバーを実行します。たとえば、ADMIN_EMAILAPP_NAME を次のように設定できます:

$ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp" fastapi run main.py

<span style="color: green;">INFO</span>:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

豆知識

1つのコマンドに複数の環境変数を設定するには、スペースで区切ってコマンドの前に並べます。

すると、admin_email の設定は "deadpool@example.com" に設定されます。

app_name"ChimichangApp" になります。

items_per_user はデフォルト値の 50 のままです。

別モジュールでの設定

大規模アプリケーション - 複数ファイル で見たように、これらの設定を別のモジュールファイルに置くこともできます。

たとえば、config.py というファイルに次のように書けます:

from pydantic_settings import BaseSettings


class Settings(BaseSettings):
    app_name: str = "Awesome API"
    admin_email: str
    items_per_user: int = 50


settings = Settings()

そして、main.py というファイルでそれを使います:

from fastapi import FastAPI

from .config import settings

app = FastAPI()


@app.get("/info")
async def info():
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }

豆知識

大規模アプリケーション - 複数ファイル で見たように、__init__.py ファイルも必要です。

依存関係での設定

場合によっては、どこでも使うグローバルな settings オブジェクトを持つ代わりに、依存関係から設定を提供すると便利なことがあります。

これは特にテスト時に有用で、依存関係を独自のカスタム設定で簡単にオーバーライドできるからです。

設定ファイル

前の例から続けると、config.py ファイルは次のようになります:

from pydantic_settings import BaseSettings


class Settings(BaseSettings):
    app_name: str = "Awesome API"
    admin_email: str
    items_per_user: int = 50
🤓 Other versions and variants

Tip

Prefer to use the Annotated version if possible.

from pydantic_settings import BaseSettings


class Settings(BaseSettings):
    app_name: str = "Awesome API"
    admin_email: str
    items_per_user: int = 50

ここでは、デフォルトのインスタンス settings = Settings() を作成していないことに注意してください。

メインアプリファイル

ここでは、新しい config.Settings() を返す依存関係を作成します。

from functools import lru_cache
from typing import Annotated

from fastapi import Depends, FastAPI

from .config import Settings

app = FastAPI()


@lru_cache
def get_settings():
    return Settings()


@app.get("/info")
async def info(settings: Annotated[Settings, Depends(get_settings)]):
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }
🤓 Other versions and variants

Tip

Prefer to use the Annotated version if possible.

from functools import lru_cache

from fastapi import Depends, FastAPI

from .config import Settings

app = FastAPI()


@lru_cache
def get_settings():
    return Settings()


@app.get("/info")
async def info(settings: Settings = Depends(get_settings)):
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }

豆知識

@lru_cache については後で説明します。

今は get_settings() が普通の関数だと考えてください。

そして、path operation 関数から依存関係として要求し、必要な場所でどこでも使えます。

from functools import lru_cache
from typing import Annotated

from fastapi import Depends, FastAPI

from .config import Settings

app = FastAPI()


@lru_cache
def get_settings():
    return Settings()


@app.get("/info")
async def info(settings: Annotated[Settings, Depends(get_settings)]):
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }
🤓 Other versions and variants

Tip

Prefer to use the Annotated version if possible.

from functools import lru_cache

from fastapi import Depends, FastAPI

from .config import Settings

app = FastAPI()


@lru_cache
def get_settings():
    return Settings()


@app.get("/info")
async def info(settings: Settings = Depends(get_settings)):
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }

設定とテスト

次に、get_settings の依存関係オーバーライドを作ることで、テスト中に別の設定オブジェクトを提供するのがとても簡単になります:

from fastapi.testclient import TestClient

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

client = TestClient(app)


def get_settings_override():
    return Settings(admin_email="testing_admin@example.com")


app.dependency_overrides[get_settings] = get_settings_override


def test_app():
    response = client.get("/info")
    data = response.json()
    assert data == {
        "app_name": "Awesome API",
        "admin_email": "testing_admin@example.com",
        "items_per_user": 50,
    }
🤓 Other versions and variants

Tip

Prefer to use the Annotated version if possible.

from fastapi.testclient import TestClient

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

client = TestClient(app)


def get_settings_override():
    return Settings(admin_email="testing_admin@example.com")


app.dependency_overrides[get_settings] = get_settings_override


def test_app():
    response = client.get("/info")
    data = response.json()
    assert data == {
        "app_name": "Awesome API",
        "admin_email": "testing_admin@example.com",
        "items_per_user": 50,
    }

依存関係オーバーライドでは、新しい Settings オブジェクトを作る際に admin_email に新しい値を設定し、その新しいオブジェクトを返します。

そして、それが使用されていることをテストできます。

.env ファイルの読み込み

変更が多くなりそうな設定が多数ある場合、環境ごとにファイルに入れて、環境変数としてそこから読み込むと便利なことがあります。

このプラクティスは十分に一般的で名前もあり、これらの環境変数は通常 .env というファイルに置かれ、そのファイルは「dotenv」と呼ばれます。

豆知識

ドット(.)で始まるファイルは、Linux や macOS のような Unix 系システムでは隠しファイルです。

ただし、dotenv ファイルは必ずしもその正確なファイル名である必要はありません。

Pydantic は外部ライブラリを使ってこの種のファイルからの読み込みをサポートしています。詳細は Pydantic Settings: Dotenv (.env) support を参照してください。

豆知識

これを機能させるには、pip install python-dotenv が必要です。

.env ファイル

次のような .env ファイルを用意できます:

ADMIN_EMAIL="deadpool@example.com"
APP_NAME="ChimichangApp"

.env から設定を読む

そして、config.py を次のように更新します:

from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    app_name: str = "Awesome API"
    admin_email: str
    items_per_user: int = 50

    model_config = SettingsConfigDict(env_file=".env")
🤓 Other versions and variants

Tip

Prefer to use the Annotated version if possible.

from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    app_name: str = "Awesome API"
    admin_email: str
    items_per_user: int = 50

    model_config = SettingsConfigDict(env_file=".env")

豆知識

model_config 属性は Pydantic の設定専用です。詳しくは Pydantic: Concepts: Configuration を参照してください。

ここでは、Pydantic の Settings クラス内で設定 env_file を定義し、使用したい dotenv ファイルのファイル名を指定しています。

lru_cacheSettings を一度だけ作成

ディスクからファイルを読むのは通常コスト(遅延)が高い処理なので、1回だけ実行して同じ設定オブジェクトを再利用し、各リクエストごとに読み直さないのが望ましいです。

しかし、次のようにするたびに:

Settings()

新しい Settings オブジェクトが作成され、その作成時に .env ファイルが再度読み込まれます。

依存関数が次のようであれば:

def get_settings():
    return Settings()

各リクエストごとにそのオブジェクトを作成し、各リクエストごとに .env ファイルを読み込むことになります。⚠️

しかし、上に @lru_cache デコレータを使っているので、Settings オブジェクトは最初に呼び出されたときに一度だけ作成されます。✔️

from functools import lru_cache
from typing import Annotated

from fastapi import Depends, FastAPI

from . import config

app = FastAPI()


@lru_cache
def get_settings():
    return config.Settings()


@app.get("/info")
async def info(settings: Annotated[config.Settings, Depends(get_settings)]):
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }
🤓 Other versions and variants

Tip

Prefer to use the Annotated version if possible.

from functools import lru_cache

from fastapi import Depends, FastAPI

from . import config

app = FastAPI()


@lru_cache
def get_settings():
    return config.Settings()


@app.get("/info")
async def info(settings: config.Settings = Depends(get_settings)):
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }

その後のリクエスト用の依存関係で get_settings() が呼ばれるたびに、get_settings() の内部コードを実行して新しい Settings オブジェクトを作るのではなく、最初の呼び出しで返されたのと同じオブジェクトを何度でも返します。

lru_cache の技術詳細

@lru_cache は、毎回関数のコードを実行して再計算するのではなく、最初に返した値を返すように、修飾する関数を変更します。

したがって、その下の関数は引数の組み合わせごとに一度だけ実行されます。そして、その各引数の組み合わせで返された値は、まったく同じ引数の組み合わせで呼び出されたときに何度でも再利用されます。

たとえば、次のような関数があるとします:

@lru_cache
def say_hi(name: str, salutation: str = "Ms."):
    return f"Hello {salutation} {name}"

プログラムは次のように実行されます:

sequenceDiagram

participant code as Code
participant function as say_hi()
participant execute as Execute function

    rect rgba(0, 255, 0, .1)
        code ->> function: say_hi(name="Camila")
        function ->> execute: execute function code
        execute ->> code: return the result
    end

    rect rgba(0, 255, 255, .1)
        code ->> function: say_hi(name="Camila")
        function ->> code: return stored result
    end

    rect rgba(0, 255, 0, .1)
        code ->> function: say_hi(name="Rick")
        function ->> execute: execute function code
        execute ->> code: return the result
    end

    rect rgba(0, 255, 0, .1)
        code ->> function: say_hi(name="Rick", salutation="Mr.")
        function ->> execute: execute function code
        execute ->> code: return the result
    end

    rect rgba(0, 255, 255, .1)
        code ->> function: say_hi(name="Rick")
        function ->> code: return stored result
    end

    rect rgba(0, 255, 255, .1)
        code ->> function: say_hi(name="Camila")
        function ->> code: return stored result
    end

今回の依存関数 get_settings() の場合、関数は引数を一切取りません。そのため、常に同じ値を返します。

この方法は、ほとんどグローバル変数のように振る舞います。しかし、依存関数を使っているので、テストのために簡単にオーバーライドできます。

@lru_cache は Python 標準ライブラリの functools の一部です。詳細は Python の @lru_cache ドキュメントを参照してください。

まとめ

Pydantic Settings を使うことで、アプリケーションの設定や構成を、Pydantic モデルの力を活かして扱えます。

  • 依存関係を使うことで、テストを簡素化できます。
  • .env ファイルを利用できます。
  • @lru_cache を使うと、各リクエストごとに dotenv ファイルを繰り返し読み込むのを避けつつ、テスト時にはオーバーライドできます。