相対パスの罠:カレントディレクトリが変わると壊れるコード

相対パスを使ったコードは、カレントディレクトリ(作業ディレクトリ)が変わると壊れる。これはよくあるバグの原因だ。

問題の例

# ❌ カレントディレクトリに依存するコード
# script.py
with open('config.json') as f:
    config = json.load(f)

このスクリプトは、スクリプトと同じディレクトリから実行すれば動く。

cd /home/user/myapp
python script.py  # 動く

しかし、別のディレクトリから実行すると動かない。

cd /home/user
python myapp/script.py  # FileNotFoundError

config.json/home/user/config.json を探しに行くが、実際には /home/user/myapp/config.json にあるからだ。

なぜカレントディレクトリが変わるのか

カレントディレクトリが変わる状況は多い。

ユーザーが異なるディレクトリからスクリプトを実行する
cron やサービスとして実行される(カレントディレクトリが / や /root になる)
IDE やテストフレームワークがプロジェクトルートから実行する
コード内で os.chdir() を呼んでいる

__file__ を使った解決方法

スクリプトの場所を基準にパスを解決すれば、どこから実行しても動く。

# ✅ スクリプトの場所を基準にする
from pathlib import Path

script_dir = Path(__file__).resolve().parent
config_path = script_dir / 'config.json'

with open(config_path) as f:
    config = json.load(f)

__file__ はスクリプト自身のパスを持つ特殊変数だ。resolve() で絶対パスに変換し、parent でディレクトリ部分を取得する。

os.path を使う場合

pathlib を使わない場合は os.path で同じことができる。

import os

script_dir = os.path.dirname(os.path.abspath(__file__))
config_path = os.path.join(script_dir, 'config.json')

パッケージ内のリソースファイル

パッケージ内のリソースファイルにアクセスする場合は importlib.resources を使う方が確実だ。

# Python 3.9 以降
from importlib.resources import files

config_text = files('mypackage').joinpath('config.json').read_text()

__file__ はパッケージが zip 化されている場合などに使えないことがあるが、importlib.resources はそのような状況でも動作する。

os.chdir() の危険性

コード内で os.chdir() を使うと、以降のすべての相対パスに影響する。

# ❌ 危険なコード
os.chdir('/tmp')
# この後のすべての相対パスが /tmp 基準になる
with open('data.txt') as f:  # /tmp/data.txt を開こうとする
    pass

どうしても chdir() が必要な場合は、コンテキストマネージャで元に戻す。

import os
from contextlib import contextmanager

@contextmanager
def working_directory(path):
    old = os.getcwd()
    try:
        os.chdir(path)
        yield
    finally:
        os.chdir(old)

# 使用例
with working_directory('/tmp'):
    # ここだけカレントディレクトリが /tmp
    pass
# 元のディレクトリに戻る

ベストプラクティス

相対パスは極力使わず、__file__ 基準の絶対パスを使う
os.chdir() は避けるか、必ず元に戻す
設定ファイルのパスは環境変数や引数で渡せるようにする