はじめに
FastAPIのプロジェクトを作り始めて、環境変数の管理をどうするか考えた。
PHPのLaravelではvlucas/phpdotenvが標準で組み込まれていて、.envを置けばenv()ヘルパーやconfig()で読めるようになっていた。Pythonではその仕組みを自分で選んで組み合わせる必要がある。
調べるとpython-dotenv、pydantic-settings、direnvなどが出てきた。それぞれ役割が違うので整理した。
そもそもなぜ環境変数で管理するか
まず前提の整理から。
# ダメな例 — 認証情報をコードに直書き
DATABASE_URL = "postgresql://admin:password123@prod-db.example.com/mydb"
SECRET_KEY = "super-secret-key-dont-share"
API_KEY = "sk-1234567890abcdef"
コードに直書きするとGitにコミットしてしまう。環境変数で管理すれば:
- 本番・ステージング・開発で設定を切り替えられる
- 認証情報がコードに混入しない
-
.envを.gitignoreに入れれば誤コミットを防げる
PHPのLaravelを使っていたので概念は理解していたが、Pythonでの具体的な方法を整理した。
os.environ — 環境変数の基本
まずPython標準の方法から。
import os
# 環境変数を読む
db_url = os.environ.get("DATABASE_URL")
debug = os.environ.get("DEBUG", "false") # デフォルト値
# 必須の環境変数(なければKeyError)
secret = os.environ["SECRET_KEY"]
# 環境変数を設定(そのプロセス内だけ)
os.environ["MY_VAR"] = "hello"
os.environ.get()はデフォルト値を指定できるのでKeyErrorを避けられる。必須の変数はos.environ["KEY"]で直接アクセスして、なければエラーにするという設計もある。
ただしos.environだけだと:
-
.envファイルを自動で読まない - 全部文字列なので型変換が必要
- 設定項目が増えると管理しにくい
という不便さがある。
python-dotenv — .envファイルを読み込む
pip install python-dotenv
# .env
DATABASE_URL=postgresql://user:pass@localhost/mydb
SECRET_KEY=dev-secret-key
DEBUG=true
PORT=8000
from dotenv import load_dotenv
import os
load_dotenv() # .envを読み込んでos.environに反映
db_url = os.environ.get("DATABASE_URL")
debug = os.environ.get("DEBUG")
port = int(os.environ.get("PORT", "8000")) # 型変換は自前
print(db_url) # postgresql://user:pass@localhost/mydb
print(debug) # "true"(文字列のまま)
load_dotenv()を一回呼ぶだけで.envの内容がos.environに反映される。PHPのvlucas/phpdotenvとほぼ同じ使い方。
複数の.envファイルを使い分ける
from dotenv import load_dotenv
import os
env = os.environ.get("APP_ENV", "development")
# 環境ごとのファイルを読み込む
load_dotenv(f".env.{env}") # .env.production, .env.staging など
load_dotenv(".env") # 共通の設定(上書きしない)
.env # 全環境共通のデフォルト値
.env.development # 開発環境
.env.staging # ステージング
.env.production # 本番(Gitに入れない)
.env.example # 設定例(Gitに入れる)
Laravelの.envと.env.exampleの使い分けと同じ発想。
load_dotenvのオプション
# すでにos.environに存在する変数は上書きしない(デフォルト)
load_dotenv(override=False)
# os.environの値を上書きする
load_dotenv(override=True)
# ファイルのパスを明示
load_dotenv("/path/to/.env")
# verbose=Trueでデバッグ情報を出力
load_dotenv(verbose=True)
override=Falseがデフォルトなので、すでにシステムの環境変数に設定されている値は.envで上書きされない。本番環境でシステム環境変数を優先させたいときに便利。
pydantic-settings — 型付きで設定を管理する
前の記事でも少し触れたが改めて整理する。
pip install pydantic-settings
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import Field
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False, # 大文字小文字を区別しない
)
# 型ヒントで自動変換される
app_name: str = "MyApp"
debug: bool = False
port: int = 8000
database_url: str
secret_key: str
allowed_hosts: list[str] = ["localhost"]
settings = Settings()
print(settings.debug) # True(文字列"true"→bool変換)
print(settings.port) # 8000(文字列"8000"→int変換)
print(type(settings.debug)) # <class 'bool'>
python-dotenvとの一番の違いは型変換が自動で行われること。
# .env
DEBUG=true
PORT=8000
ALLOWED_HOSTS=["localhost", "127.0.0.1"]
DEBUG=trueがboolのTrueに、PORT=8000がintの8000に自動変換される。PHPのenv('DEBUG', false)は文字列"true"とfalseを比較してしまうバグが起きやすかったが、pydantic-settingsはその問題がない。
FastAPIとの組み合わせ
from functools import lru_cache
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env")
database_url: str
secret_key: str
debug: bool = False
port: int = 8000
@lru_cache
def get_settings() -> Settings:
return Settings()
# FastAPIのDependsで注入する
from fastapi import FastAPI, Depends
app = FastAPI()
@app.get("/info")
def get_info(settings: Settings = Depends(get_settings)):
return {
"app_name": settings.app_name,
"debug": settings.debug,
}
@lru_cacheでSettingsインスタンスをキャッシュして毎回.envを読み直さないようにする。Depends(get_settings)でDIとして各エンドポイントに注入するパターンが定番。
設定のバリデーション
from pydantic import Field, field_validator
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
port: int = Field(8000, ge=1024, le=65535)
secret_key: str = Field(..., min_length=32)
log_level: str = "INFO"
@field_validator("log_level")
@classmethod
def validate_log_level(cls, v: str) -> str:
allowed = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
if v.upper() not in allowed:
raise ValueError(f"log_levelは{allowed}のいずれかを指定してください")
return v.upper()
Pydanticのバリデーション機能がそのまま使えるので、設定値の制約も型定義と一緒に書ける。
direnv — ディレクトリごとに環境変数を自動切替
python-dotenvやpydantic-settingsはPythonコード内での話。direnvはシェルレベルで環境変数を自動設定するツール。
# Mac
brew install direnv
# .zshrcに追記
eval "$(direnv hook zsh)"
# プロジェクトのルートで
echo 'export DATABASE_URL="postgresql://user:pass@localhost/mydb"' > .envrc
echo 'export DEBUG="true"' >> .envrc
direnv allow # このディレクトリの.envrcを許可
direnv: loading .envrc
direnv: export +DATABASE_URL +DEBUG
ディレクトリに移動すると自動で環境変数が設定され、ディレクトリを出ると自動で解除される。
cd myproject/
# → 環境変数が自動でセットされる
cd ../
# → 環境変数が自動で解除される
.envrcでpyenvやvenvも管理
# .envrc
use python 3.12.3 # pyenvのバージョン指定
layout python # venvを自動で作成・有効化
export DATABASE_URL="postgresql://user:pass@localhost/mydb"
export SECRET_KEY="dev-secret-key"
export DEBUG="true"
layout pythonを書くと.direnv/にvenvが自動で作られて、ディレクトリに入ると自動でactivateされる。毎回source .venv/bin/activateするのが不要になった。これが地味に便利。
.envrcのセキュリティ
# .gitignoreに必ず追加
.envrc
.direnv/
.envrcに認証情報を書く場合はGitignoreに追加する。設定例だけ残したい場合は.envrc.exampleを作る慣習がある。
3つのツールの使い分け
| ツール | 役割 | 使うタイミング |
|---|---|---|
os.environ |
環境変数の読み書き(標準) | シンプルな用途 |
python-dotenv |
.envをos.environに読み込む |
軽量に使いたいとき |
pydantic-settings |
型付き・バリデーション付きで設定管理 | FastAPIなど型を活用する場合 |
direnv |
シェルレベルで環境変数を自動管理 | 複数プロジェクトを切り替える開発環境 |
個人的にはFastAPIプロジェクトではpydantic-settings + direnvの組み合わせが一番快適だった。
-
pydantic-settingsでコード内の型安全な設定管理 -
direnvでプロジェクト切り替え時の環境変数自動管理
python-dotenvはpydantic-settingsを使うなら不要になる(pydantic-settingsが内部で.envを読んでくれる)。
.gitignoreに必ず入れるもの
# .gitignore
.env
.env.*
!.env.example
!.env.*.example
.envrc
.direnv/
!.env.exampleで例外としてexampleファイルはGitに含める。
.env.exampleを用意する
# .env.example(Gitに含める)
DATABASE_URL=postgresql://user:pass@localhost/mydb
SECRET_KEY= # 必須:32文字以上のランダムな文字列
DEBUG=false
PORT=8000
LOG_LEVEL=INFO
ALLOWED_HOSTS=["localhost"]
新しいメンバーがプロジェクトに入ったときに.env.exampleをコピーして値を埋めるだけで済む。Laravelと同じ運用。
PHPのdotenv(vlucas/phpdotenv)との比較
| 項目 | PHP(vlucas/phpdotenv) | Python |
|---|---|---|
| .envの読み込み | Dotenv::createImmutable() |
load_dotenv() / pydantic-settings
|
| 型変換 | なし(全部文字列) |
pydantic-settingsで自動 |
| バリデーション |
required() / allowedValues()
|
pydantic-settings + Field
|
| 環境ごとの切替 |
.env.testingなど |
.env.{env} / direnv
|
| Gitignore | .env |
.env, .envrc
|
PHPは型変換がないのでenv('DEBUG') === 'true'のような比較が必要だったが、pydantic-settingsはboolに自動変換されるので型安全に使える。
まとめ
-
os.environは標準だが型変換・バリデーションがない -
python-dotenvで.envを読み込める。シンプルな用途に -
pydantic-settingsで型安全・バリデーション付きの設定管理。FastAPIと相性がいい -
direnvでシェルレベルの自動切替。複数プロジェクトを行き来するときに快適 -
.envと.envrcはGitignoreに入れて.env.exampleを代わりに共有する
Laravelのようにフレームワークがデフォルトでdotenvを組み込んでいないので最初は戸惑ったが、自分で選んで組み合わせる分、プロジェクトの要件に合わせた構成が作れる。