til

Python

モジュール・パッケージ

実験は_src/moduleを参照。

Pythonにおいて、.py ファイルをモジュール、モジュールの含まれたディレクトリをパッケージと呼ぶ。

同じパッケージという名前で、次のような異なった概念がある。混乱のもとになっている。

例えば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']>

install

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

editable install

実験は_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.pyinstall_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!

import

Pythonは、パッケージ・モジュールを配置すべきパスをモジュール検索パスとして提供している。モジュール検索パスはsitesys.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']

モジュール検索パスは、とりあえず次のとおり構成される。

  1. カレントディレクトリ
  2. PYTHONPATH環境変数で指定したパス
  3. site-packagesディレクトリ
  4. 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.pypython -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()]"

次に、そのパスがなぜモジュール検索パスに含まれているかを確認しましょう。

これは便利な方法が発見できなかったので、モジュール検索パスの構成を参照して手動で切り分けます。

srcレイアウトを採用したら、パッケージのインポートができなくなった

私が実際に[blender-mcp-senpai](https://github.com/xhiroga/blender-mcp-senpai)で遭遇したケース。srcレイアウトを採用した上で、uv run python src/blender-mcp-senpai/main.pyのように実行すると、ImportErrorが発生しました。

この場合、srcレイアウトを採用する前はどのパスからパッケージをインポートしていたかを特定すべきです。

ちなみに私の場合は、カレントディレクトリ経由の絶対importを、暗黙的相対importと勘違いしていたことが原因でした。

非パッケージまたはフラットレイアウトからsrcレイアウトに切り替える際の注意は?