実験は_src/module
を参照。
Pythonにおいて、.py
ファイルをモジュール、モジュールの含まれたディレクトリをパッケージと呼ぶ。
同じパッケージという名前で、次のような異なった概念がある。混乱のもとになっている。
pip install $PACKAGE_NAME
で指定するパッケージimport $PACKAGE_NAME
で指定するパッケージ例えばPillow
の場合、配布パッケージはPillow
であり、インポートパッケージはPIL
である。
インポートパッケージには2種類ある。Python3.2以前から利用できたregular package
と、Python3.3から利用できるnamespace package
である。
(なお、regular package
という用語はPEP420から引用した)
% uv run python
>>> from module import spam, ham
>>> spam
<module 'module.spam' from '/home/hiroga/Documents/GitHub/til/software-engineering/python/_src/module/src/module/spam/__init__.py'>
>>> ham
<module 'module.ham' (namespace) from ['/home/hiroga/Documents/GitHub/til/software-engineering/python/_src/module/src/module/ham']>
pip install $PACKAGE_NAME
でパッケージをインストールすると、その配布パッケージに対応するインポートパッケージがsite-packages
にインストールされる。
単一の配布パッケージが複数のインポートパッケージを持つこともある。
$ uv add attrs
$ uv run python -c "import attr"
$ uv run python -c "import attrs"
配布パッケージを用いずにインポートパッケージを追加する方法もある。.whl
ファイルやGitHub
のリポジトリを直接指定すればよい。
$ make somepackage/dist/somepackage-0.1.0-py3-none-any.whl
$ uv add somepackage/dist/somepackage-0.1.0-py3-none-any.whl
実験は_src/editable-install
を参照。
カレントディレクトリをパッケージとしてinstallすると、site-packages
ディレクトリを経由してパッケージを利用できる。そのためimport文のモジュール名をパッケージ名から始めることができる。
$ python3 -m venv .venv
$ .venv/bin/pip install -e .
$ .venv/bin/python src/editable_install/main.py
Hello, World!
この際、site-packages
ディレクトリには.pth
ファイルが作成される。
$ cat .venv/lib/python3.13/site-packages/_editable_install.pth
/home/hiroga/Documents/GitHub/til/software-engineering/python/_src/editable-install/src
CLIのあるOSSなど、リポジトリがエントリーポイントとインポートパッケージの両方を持つケースがある。そうしたリポジトリを開発する場合は、pip install -e .
が推奨されている。Is setup.py deprecated?も参照。
$ rm -rf .venv
$ python3 -m venv .venv
$ cat requirements.txt
-e .
$ .venv/bin/pip install -r requirements.txt
$ cat .venv/lib/python3.13/site-packages/_editable_install.pth
/home/hiroga/Documents/GitHub/til/software-engineering/python/_src/editable-install/src
$ .venv/bin/python src/editable_install/main.py
Hello, World!
また、extraを指定することでpip install -e .[dev]
のような運用もできる。なお、setup.py
のinstall_requires
ではバージョン固定などができなかったことから、requrements-dev.txt
を定義し、その中で-e .
をラップすることで細やかな指定を行うテクニックがある。
$ rm -rf .local
$ mkdir -p .local
$ git -C .local clone https://github.com/psf/requests.git
$ cd .local/requests
$ python3 -m venv .venv
$ .venv/bin/pip install -r requirements-dev.txt
$ cat .venv/lib/python3.13/site-packages/__editable__.requests-2.32.3.pth
/home/hiroga/Documents/GitHub/til/software-engineering/python/_src/.local/requests/src
なお、uv
を利用している場合は、[build-system]
が宣言されているならばuv sync
の際に自動的に-e .
を実行する。
$ rm -rf .venv && rm uv.lock
$ uv sync --verbose
DEBUG uv 0.7.3 (Homebrew 2025-05-07)
...
Creating virtual environment at: .venv
...
DEBUG Adding direct dependency: editable-install*
DEBUG Directory source requirement already cached: editable-install==0.1.0 (from file:///home/hiroga/Documents/GitHub/til/software-engineering/python/_src/editable-install)
Installed 1 package in 3ms
+ editable-install==0.1.0 (from file:///home/hiroga/Documents/GitHub/til/software-engineering/python/_src/editable-install)
$ uv run python src/editable_install/main.py
Hello, World!
Pythonは、パッケージ・モジュールを配置すべきパスをモジュール検索パスとして提供している。モジュール検索パスはsite
やsys.path
で確認できる。
$ .venv/bin/python -m site
$ uv run python -m site
sys.path = [
'/home/hiroga/Documents/GitHub/til/software-engineering/python/_src/module',
'/home/hiroga/.local/share/uv/python/cpython-3.13.3-linux-x86_64-gnu/lib/python313.zip',
'/home/hiroga/.local/share/uv/python/cpython-3.13.3-linux-x86_64-gnu/lib/python3.13',
'/home/hiroga/.local/share/uv/python/cpython-3.13.3-linux-x86_64-gnu/lib/python3.13/lib-dynload',
'/home/hiroga/Documents/GitHub/til/software-engineering/python/_src/module/.venv/lib/python3.13/site-packages',
'/home/hiroga/Documents/GitHub/til/software-engineering/python/_src/module/src',
]
USER_BASE: '/home/hiroga/.local' (exists)
USER_SITE: '/home/hiroga/.local/lib/python3.13/site-packages' (doesn't exist)
ENABLE_USER_SITE: False
$ .venv/bin/python -c "import sys; print(sys.path)"
$ uv run python -c "import sys; print(sys.path)"
['', '/home/hiroga/.local/share/uv/python/cpython-3.13.3-linux-x86_64-gnu/lib/python313.zip', '/home/hiroga/.local/share/uv/python/cpython-3.13.3-linux-x86_64-gnu/lib/python3.13', '/home/hiroga/.local/share/uv/python/cpython-3.13.3-linux-x86_64-gnu/lib/python3.13/lib-dynload', '/home/hiroga/Documents/GitHub/til/software-engineering/python/_src/.venv/lib/python3.13/site-packages', '/home/hiroga/Documents/GitHub/til/software-engineering/python/_src/module/src']
モジュール検索パスは、とりあえず次のとおり構成される。
PYTHONPATH
環境変数で指定したパスsite-packages
ディレクトリsite-packages
以下の.pth
ファイルで指定したパス.pth
ファイルは、歴史的にはPYTHONPATH
の代替手段として登場したらしい。1行に1つのパスを含むシンプルなファイルである。
$ cat .venv/lib/python3.13/site-packages/_module.pth
/home/hiroga/Documents/GitHub/til/software-engineering/python/_src/module/src
Python3でモジュールをimportするとき、モジュール名の頭に.
を付けないなら、それは絶対importである。アプリケーション開発のレイアウトではエントリーポイントはプロジェクトルートに置かれるのが通例なので、プロジェクトルートからの相対importのようにも見えるが、異なる。モジュール検索パス内のカレントディレクトリからの絶対importである。
パッケージを配布するとき、そのパッケージがパッケージ管理ツール(pip, uv, etc…)によってインストールされるなら、パッケージはsite-packages
ディレクトリ経由でimportされる。なお、そうではない例としては、アプリケーション拡張機能(Blender, ComfyUI, etc…)など独自の方法でソースコードを管理する場合が考えられる。
したがって、配布用のパッケージでは、カレントディレクトリからの絶対importを採用すると、インストール時に動かなくなることがある。これを避けるため、パッケージ開発においてはEditalbe install
を用いることでsite-packages
ディレクトリから参照するよう統一することがベストプラクティスになっている。
StackOverFlowの回答も参照。
python -m main.py
や python -m http.server
のようなモジュール実行は、Python2.4.1で登場した。
Pythonのユースケースが広がるにつれて、WebアプリケーションフレームワークやCLIツールなど、ライブラリがエントリーポイントを担うケースが登場した。その際もモジュールのimport時と同様に正確なパスを知らなくても使いたいという要望が生まれたのだろう。そうした背景から-m
オプションが導入された。
モジュール実行の場合、エントリーポイントを除くアプリケーションのコード内でも相対importを用いることが可能になる。パッケージ開発ではEditable install
を用いるのがベストプラクティスであると紹介したが、-m
オプションでの開発と相対importの組み合わせも広がっていくかもしれない。
なお、Editable install
を用いる場合、エントリーポイント以外のアプリケーションコード内で相対importが可能になる。ややこしいので避けた方が良さそうだ。
Pythonのソースコードの配布は、初期にはソースコードを直接共有する形で行われた。その後、リポジトリから配布用のソースコードをビルドする手順としてsetup.py
が導入された。
ビルド手順の導入に伴い、GitHubなどのVCSからパッケージを直接ダウンロードすることが可能になった。また、ビルドツールの多様化や静的解析ツールからの要望を受けてsetup.py
の代わりに静的設定ファイル(setup.cfg
, pyproject.toml
)が導入される。
初めに、元になったモジュール検索パスを確認しましょう。
$ uv run python -c "import importlib, pkgutil; [print(importlib.util.find_spec(mod.name)) for mod in pkgutil.iter_modules()]"
次に、そのパスがなぜモジュール検索パスに含まれているかを確認しましょう。
これは便利な方法が発見できなかったので、モジュール検索パスの構成を参照して手動で切り分けます。
私が実際に[blender-mcp-senpai](https://github.com/xhiroga/blender-mcp-senpai)
で遭遇したケース。srcレイアウトを採用した上で、uv run python src/blender-mcp-senpai/main.py
のように実行すると、ImportError
が発生しました。
この場合、srcレイアウトを採用する前はどのパスからパッケージをインポートしていたかを特定すべきです。
ちなみに私の場合は、カレントディレクトリ経由の絶対importを、暗黙的相対importと勘違いしていたことが原因でした。
Editable install
の必要性についてドキュメントに記載します