モダンPython基礎 #7 モジュール/パッケージと pyproject.toml

六編の間、一つのファイルの中でコードを書いてきました。今回の最後の記事は 複数のファイルに分割し、それらをプロジェクトとしてまとめる方法import システム、モジュールとパッケージ、そしてそのすべてを定義する pyproject.toml です。

  • #1 はじめ方と uv セットアップ
  • #2 変数、基本型、型ヒント
  • #3 制御フロー
  • #4 コレクションと内包表記
  • #5 関数 — 引数パターン
  • #6 エラーと例外処理
  • #7 モジュール / パッケージと pyproject.toml ← 今回

モジュール — ファイル一つがモジュール #

Python では .py ファイル一つがそのままモジュール です。他のファイルから import で取り込んで使います。

math_utils.py
def add(a: int, b: int) -> int:
    return a + b

def multiply(a: int, b: int) -> int:
    return a * b

PI = 3.141592
main.py
import math_utils

print(math_utils.add(2, 3))     # 5
print(math_utils.PI)            # 3.141592

四つの import 形式 #

import バリエーション
# 1. モジュール丸ごと
import math_utils
math_utils.add(1, 2)

# 2. 別名
import math_utils as mu
mu.add(1, 2)

# 3. 特定の名前だけ取り込む
from math_utils import add, PI
add(1, 2)

# 4. 別名 + 取り込み
from math_utils import add as plus
plus(1, 2)

from m import * はほぼ使わない #

🚫 普通は避ける
from math_utils import *

名前の衝突、どこから来た名前か追跡不能、静的解析器の混乱。新しいコードではほぼ使いません。例外はインタラクティブシェルで一時的に使う場面くらいです。

パッケージ — ディレクトリがパッケージ #

複数のモジュールをまとめたいときは ディレクトリ で作ります。

パッケージ構造
my_app/
├── __init__.py
├── auth.py
├── db.py
└── handlers/
    ├── __init__.py
    ├── user.py
    └── post.py

この構造で:

パッケージ import
from my_app import auth
from my_app.db import connect
from my_app.handlers.user import create_user

__init__.py — 二つの役割 #

__init__.py は二つのことをします。

  1. このディレクトリがパッケージであることを示す (歴史的役割 — Python 3.3 以降はなくても namespace package として扱われる)
  2. パッケージが import されたときに実行されるコード — 公開 API を整理したり初期化したり
my_app/__init__.py
"""my_app — サンプルアプリケーション。"""

from my_app.auth import login, logout
from my_app.db import connect

__all__ = ["login", "logout", "connect"]
__version__ = "0.1.0"

こうすると呼び出し側は:

短くなった import
from my_app import login, connect

内部構造 (auth モジュール、db モジュール) が露出しません。公開 API を一箇所で定義する パターンです。

__all__from my_app import * のときに取り込む名前のホワイトリストですが、実際には 公開 API を明示するドキュメント としての役割でより頻繁に使われます。

namespace package — __init__.py がなければ #

3.3+ からは __init__.py のないディレクトリもパッケージになります (implicit namespace package)。短所:

  • 初期化コードを置く場所がない
  • 一部のツールが認識しない場合がある (古いバージョン)

新しいパッケージを作るときは __init__.py を置く 方が安全です。空でも構いません (pass 一行も不要、空ファイルで OK)。

絶対 import vs 相対 import #

同じパッケージの中で別のモジュールを取り込むには二つの方法があります。

my_app/handlers/user.py
# 絶対 import
from my_app.db import connect
from my_app.auth import current_user

# 相対 import
from ..db import connect
from ..auth import current_user

.. は「一つ上」、. は「同じディレクトリ」です。

両方できますが絶対 import を推奨 します (PEP 8)。どこから取り込んでいるか一目で分かり、ファイルを移動するときも壊れにくいです。相対 import はパッケージ内部の短い距離だけで使う程度が無難です。

__main__ — モジュールを直接実行するとき #

スクリプトとして実行するときだけ動くコードを置く場所です。

cli.py
def main():
    print("hello!")

if __name__ == "__main__":
    main()

このファイルは二つの方法で使えます。

直接実行
$ uv run cli.py
hello!         ← __name__ が '__main__'
他のファイルから import
import cli
cli.main()    # 明示的に呼べば動く
# import 時点では main() は呼ばれない

if __name__ == "__main__": がないと import しただけで main が実行されて 副作用が発生します。常にこのガードを置く のが模範です。

パッケージ単位の実行 — __main__.py #

パッケージ自体を実行可能にするには:

実行可能なパッケージ
my_cli/
├── __init__.py
├── __main__.py     ← ここに main エントリ
└── core.py
my_cli/__main__.py
from my_cli.core import run

if __name__ == "__main__":
    run()

これで:

パッケージとして実行
$ uv run python -m my_cli

CLI ツールを作るときによく使うパターンです。

標準ライブラリ — 同じ import システム #

標準ライブラリも同じように import します。別途インストールせずそのまま使えます。

よく使う標準モジュール
import os                # ファイルシステム
import sys               # インタプリタ、argv
import json              # JSON
import re                # 正規表現
import datetime          # 日付 / 時刻
import pathlib           # パスオブジェクト
from collections import Counter, defaultdict
from itertools import chain, groupby
from functools import lru_cache, partial

標準ライブラリは 公式ドキュメント が最も速い参照先です。

pyproject.toml — プロジェクトの単一の定義 #

#1uv init で作ったあのファイルです。今回本格的に見ていきます。

pyproject.toml
[project]
name = "my-app"
version = "0.1.0"
description = "サンプルアプリケーション"
readme = "README.md"
requires-python = ">=3.14"
authors = [
    { name = "Curtis", email = "you@example.com" },
]
license = { text = "MIT" }
dependencies = [
    "httpx>=0.28",
    "pydantic>=2.10",
]

[project.optional-dependencies]
docs = ["mkdocs>=1.6"]

[dependency-groups]
dev = [
    "pytest>=8.0",
    "ruff>=0.7",
    "pyright>=1.1",
]

[project.scripts]
my-app = "my_app.cli:main"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

主要なセクションを一つずつ。

[project] — PEP 621 メタデータ #

名前、バージョン、説明、Python バージョン要件、作者、ライセンス、ランタイム依存関係

[project.optional-dependencies] — オプションの依存関係 #

pip install 'my-app[docs]' のような形でユーザーが選んでインストールできるバンドルです。ライブラリを配布するときによく使います。

[dependency-groups] — 開発 / テスト用 #

PEP 735 で標準化された仕組み。配布には入らないが開発時には必要な ツール群をまとめます。uv が uv add --dev で自動的に追加します。

[project.scripts] — CLI エントリポイント #

このセクションがあると、パッケージインストール時に コンソールコマンドが自動的に作られます。

[project.scripts]
my-app = "my_app.cli:main"

インストール後:

$ my-app
# my_app.cli モジュールの main 関数が実行される

CLI ツールを作るならこのセクションが核心です。

[build-system] — ビルドバックエンド #

PyPI に上げるパッケージなら必要。hatchlingsetuptoolspdm-backend など複数の選択肢がありますが、新規プロジェクトは hatchling が軽くて無難です。

pyproject.toml の中にツール設定も #

リンタ / フォーマッタ / 型チェッカーも同じファイルに設定を置きます。

ツール設定
[tool.ruff]
line-length = 100
target-version = "py314"

[tool.ruff.lint]
select = ["E", "F", "I", "N", "UP", "B"]

[tool.pyright]
typeCheckingMode = "strict"
pythonVersion = "3.14"

[tool.pytest.ini_options]
testpaths = ["tests"]

setup.cfg.flake8mypy.inipytest.ini のような散らばっていたファイルが 一つにまとまります。

uv の日常コマンド — もう一度 #

#1 で見たものに加えて、よく使う流れ:

日常の流れ
# 新しいプロジェクト
uv init my-app --python 3.14

# 依存関係
uv add httpx pydantic
uv add --dev pytest ruff pyright
uv remove old-package

# 同期 (別のマシンで / 新しく取得したとき)
uv sync

# 実行
uv run python main.py
uv run pytest
uv run ruff check
uv run pyright

# 一時実行 (プロジェクト依存ではないツール)
uvx ruff check .          # 一度だけ実行、環境を汚さない

# Python 自体のアップグレード
uv python install 3.14

uvx隔離された環境で一度だけ実行 するコマンドです。pipx と同じ位置で、グローバルにツールをインストールしないので安全です。

パッケージを PyPI に配布するには #

ここまでセットアップが済んでいれば、配布は一つか二つのコマンドです。

ビルド + 配布
uv build                          # dist/ に wheel + sdist を生成
uvx twine upload dist/*           # PyPI にアップロード

(twine の代わりに uv publish を使う流れもありますが、安定性の面で twine がまだ標準です。)

シリーズの締めくくり #

今回整理したもの:

  • ファイル一つがモジュール、ディレクトリがパッケージ
  • import の四つの形 — import ximport x as yfrom x import yfrom x import y as z
  • __init__.py — パッケージの目印 + 初期化 + 公開 API 整理 (__all__)
  • 絶対 import を推奨、相対 import は短い距離だけ
  • if __name__ == "__main__": ガードは常に
  • パッケージ単位の実行は __main__.py + python -m my_pkg
  • pyproject.toml 一つのファイルにメタデータ + 依存関係 + スクリプト + ツール設定すべて
  • [dependency-groups] で開発依存を分離
  • [project.scripts] でコンソールコマンドを自動生成
  • uv syncuv runuvx が日常のワークフロー

シリーズ全体の振り返り #

7 編を経てモダン Python の 基礎インフラ が整いました。少なくとも次のようなコードは自分で書いて読めます。

ここまでで揃った道具で書ける一行
from collections.abc import Callable

type UserId = int

def find_users(
    ids: list[UserId],
    *,
    transform: Callable[[UserId], str] = str,
    skip_invalid: bool = True,
) -> dict[UserId, str]:
    """ID リストから変換済みユーザーマップを作る。"""
    result: dict[UserId, str] = {}
    for uid in ids:
        try:
            result[uid] = transform(uid)
        except ValueError:
            if not skip_invalid:
                raise
    return result

型ヒント、| union、組み込みジェネリクス、keyword-only、Callable、例外処理、docstring — すべてこのシリーズで扱った道具たちです。

次のシリーズ #

次は モダン Python 中級 シリーズです。次のテーマが入ります。

  1. dataclass と __slots__
  2. typing 本格 — Generic、Protocol、TypedDict、Literal
  3. コンテキストマネージャ (withcontextlib) — 今回のシリーズの finally 位置でよく言及されたあの道具
  4. イテラブル / ジェネレータ / yield from
  5. デコレータパターン
  6. パターンマッチングの深層 (今回の #3 match-case の次の段階)
  7. 非同期入門 (asyncio 基礎)

この基礎シリーズがその上にそのままつながる足場になるよう設計した流れなので、中級も途切れずに入っていけるはずです。

X