0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Pythonでの環境変数管理 — .envとpython-dotenvとdirenvを整理した

0
Posted at

はじめに

FastAPIのプロジェクトを作り始めて、環境変数の管理をどうするか考えた。

PHPのLaravelではvlucas/phpdotenvが標準で組み込まれていて、.envを置けばenv()ヘルパーやconfig()で読めるようになっていた。Pythonではその仕組みを自分で選んで組み合わせる必要がある。

調べるとpython-dotenvpydantic-settingsdirenvなどが出てきた。それぞれ役割が違うので整理した。


そもそもなぜ環境変数で管理するか

まず前提の整理から。

# ダメな例 — 認証情報をコードに直書き
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=trueboolTrueに、PORT=8000int8000に自動変換される。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_cacheSettingsインスタンスをキャッシュして毎回.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-dotenvpydantic-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 .envos.environに読み込む 軽量に使いたいとき
pydantic-settings 型付き・バリデーション付きで設定管理 FastAPIなど型を活用する場合
direnv シェルレベルで環境変数を自動管理 複数プロジェクトを切り替える開発環境

個人的にはFastAPIプロジェクトではpydantic-settings + direnvの組み合わせが一番快適だった。

  • pydantic-settingsでコード内の型安全な設定管理
  • direnvでプロジェクト切り替え時の環境変数自動管理

python-dotenvpydantic-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-settingsboolに自動変換されるので型安全に使える。


まとめ

  • os.environは標準だが型変換・バリデーションがない
  • python-dotenv.envを読み込める。シンプルな用途に
  • pydantic-settingsで型安全・バリデーション付きの設定管理。FastAPIと相性がいい
  • direnvでシェルレベルの自動切替。複数プロジェクトを行き来するときに快適
  • .env.envrcはGitignoreに入れて.env.exampleを代わりに共有する

Laravelのようにフレームワークがデフォルトでdotenvを組み込んでいないので最初は戸惑ったが、自分で選んで組み合わせる分、プロジェクトの要件に合わせた構成が作れる。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?