nikkie-ftnextの日記

イベントレポートや読書メモを発信

pip installとpython -m pip install、pytestとpython -m pytestは何が違う? python -mとsys.pathの秘密

はじめに

Act-4、最高だった...😭 余韻の真っ只中のnikkieです。

表題の件について、違いを知ったので記します。
鍵を握るのはsys.pathです

目次

pytestのドキュメントより

ドキュメント「How to invoke pytest」に「Calling pytest through python -m pytest」があります。
https://docs.pytest.org/en/8.0.x/how-to/usage.html#calling-pytest-through-python-m-pytest

python -m pytest [...]

This is almost equivalent to invoking the command line script pytest [...] directly, except that calling via python will also add the current directory to sys.path.

pytest [...]との違いは、python -m pytest [...]現在のディレクトリをsys.pathに追加する点です。
この点以外は同じと理解しました。

sys.pathのドキュメント

https://docs.python.org/ja/3/library/sys.html#sys.path

モジュールを検索するパスを示す文字列のリスト。

ここに記載があります。

python -m module command line: prepend the current working directory.

現在の作業ディレクトリをリストの先頭に追加する

PyCon APACのトークでも聞いていました。

sys.pathの先頭には
カレントディレクトがセットされる

結論:pytestとpython -m pytestはここが違う!

  • モジュールが提供するコマンド(例:pytest)は、sys.pathを変更しない
  • python -m module(例:python -m pytest)は、sys.pathの先頭に現在のディレクトリを追加するという変更をする

sys.pathの変更以外は同じです

sys.pathが変わることを動作確認

簡単なライブラリを作って確認します。
こういうとき、(ライブラリの雛形があるという点で)unko1は役に立ちますね。

sys.pathを確認する実装

unko/__main__.py を実装しました

import sys


def main():
    for path in sys.path:
        print(path)


if __name__ == "__main__":
    main()

pyproject.tomlでunkoコマンドを定義します

[project.scripts]
unko = "unko.__main__:main"

全容はこちらからどうぞ:
https://github.com/ftnext/unko/compare/main...example/python-m

動作確認

違いが分かりやすいようにunkoのルートではなく、tmpディレクトリを作ってそこで実行しました。

% unko
/.../unko/.venv/bin
/.../.pyenv/versions/3.12.0/lib/python312.zip
/.../.pyenv/versions/3.12.0/lib/python3.12
/.../.pyenv/versions/3.12.0/lib/python3.12/lib-dynload
/.../unko/.venv/lib/python3.12/site-packages

(仮想環境.venvのbinがsys.pathに入っているのは、また別の理由がありそうですね)

% python -m unko
/.../unko/tmp
/.../.pyenv/versions/3.12.0/lib/python312.zip
/.../.pyenv/versions/3.12.0/lib/python3.12
/.../.pyenv/versions/3.12.0/lib/python3.12/lib-dynload
/.../unko/.venv/lib/python3.12/site-packages

単にunkoコマンドとの違いは、カレントディレクトリ(/.../unko/tmp)がsys.pathの先頭にあるかいなかですね。

思い出した過去記事

(1)コマンドラインで実行だけするPythonアプリケーションについては、pipxというツールで管理する方法があります。

原理的にはpython -mで実行もできるわけですが、我が身を振り返るとコマンドで実行することが多いですね。
コマンド自体がきれいに作られているから、でしょうか。

(2)sys.pathは環境変数PYTHONPATHでも設定できる

% PYTHONPATH=/spam/ham/egg unko
/.../unko/.venv/bin
/spam/ham/egg
/.../.pyenv/versions/3.12.0/lib/python312.zip
/.../.pyenv/versions/3.12.0/lib/python3.12
/.../.pyenv/versions/3.12.0/lib/python3.12/lib-dynload
/.../unko/.venv/lib/python3.12/site-packages
% PYTHONPATH=/spam/ham/egg python -m unko
/.../unko/tmp
/spam/ham/egg
/.../.pyenv/versions/3.12.0/lib/python312.zip
/.../.pyenv/versions/3.12.0/lib/python3.12
/.../.pyenv/versions/3.12.0/lib/python3.12/lib-dynload
/.../unko/.venv/lib/python3.12/site-packages

python -mを実行したカレントディレクトリは、PYTHONPATHで指定したパスより前に入るんですね。

終わりに

pytestとpython -m pytestや、pip installとpython -m pip installの違いを知りました。
ぼんやりと「使い手の好みくらいしか差はないんじゃないか」と考えていましたが、sys.pathにカレントディレクトリを追加するかしないかという振る舞いの違いがありました。

pipxに心惹かれているのもあって私自身はpython -m pytestのようなコマンドはあまり利用しないんじゃないかと思いますが、裏でsys.pathを変更しているという点は心に留めておきたいと思っています


  1. 出自