メインコンテンツへスキップ

__init__.pyのベストプラクティスとアンチパターンについて

はじめに

__init__.py は「このフォルダは Python パッケージだよ」と示す合図であり、同時に「パッケージの入口」を整える場所です。ここをどう書くかで、インポート速度・循環依存・公開APIのわかりやすさがガラッと変わります。この記事では、__init__.py の歴史と役割を押さえつつ、実務で効くベストプラクティスと避けたいアンチパターンをまとめます。

🧭 背景:なぜ __init__.py は存在するのか

  • もともと Python は「フォルダ=ただのフォルダ」。そこに __init__.py を置くことで「パッケージ」として解釈され、import pkg が通るようになりました。

  • 後に “暗黙の名前空間パッケージ(implicit namespace package)” が導入され、__init__.py が無くてもパッケージとして扱えるケースが生まれました(複数ディレクトリにまたがるパッケージを自然に扱える)。

  • つまり今は「置かなくても動く」場面がある一方で、「置くとできること(公開APIの整形や軽い初期化)」もあります。役割を理解して使い分けるのがコツです。

暗黙の名前空間パッケージは「配布や分割」に便利ですが、__init__.py が無いぶん“入口での調整”ができません。用途に応じて選びます。

🧰 役割まとめ:__init__.py で“やっていいこと”

  • 公開 API を整える(再エクスポートして from pkg import Foo を可能にする)

  • パッケージのメタデータを置く(__version__ など)

  • 軽い初期化(ログの名前空間設定、プラグインの自動登録など“軽い”処理)

  • 型チェックや補完のための“見せ方”調整(__all__ やシンボルの並べ替え)

最小限のエクスポート整理+メタデータ記載に留めると、保守がラクで安全です。

🧪 置くか・置かないかの判断基準

  • 置かない(名前空間パッケージ向き):

    • サブパッケージを別リポジトリや別ディレクトリで配布・拡張したい

    • 入口での初期化や API 整形が不要

  • 置く(従来パッケージ向き):

    • 使い手にとっての“入口”をスッキリさせたい(再エクスポートなど)

    • 軽い初期化やメタデータを入れたい

チーム開発では「公開 API の門番」を置きたいことが多く、__init__.py を作るほうが現実的です。

✅ ベストプラクティス

  1. 薄く保つ(Heavy is bad)
    初期化は“軽い”範囲に限定。時間のかかる import・I/O・ネットワークアクセスは厳禁。

  2. 公開 API を明示する

    • 再エクスポートする場合は明示的に書く:
      例)from .core import Foo, Bar

    • さらに __all__ = ["Foo", "Bar"] で補完と静的解析を助ける。

  3. 相対インポートを使う

    • パッケージ内は from .submodule import Foo のように相対で。移動やリネームに強く、循環も気づきやすい。

  4. メタデータをここに集約

    • __version__ = "1.2.3" のように、利用者が import pkg; pkg.__version__ で確認できるようにする。

    • バージョンは、ビルド時に書き換える・importlib.metadata から読む、どちらに寄せるかをチームで統一。

  5. 遅延インポート(必要なら)

    • コールドスタートが重い場合、関数内で import して遅延させる。

    • ただしやり過ぎは可読性低下。入口での“重い再エクスポート”をやめるだけでも大抵は改善します。

  6. ドキュメントストリングを入口に

    • パッケージ docstring に「このパッケージの何が嬉しいか」「主要コンポーネント」を簡潔に記すと親切。

  7. 型の見え方を整える

    • 再エクスポートで “外から見える型” を整理しておくと、IDE 補完が気持ちよく決まる。

    • 必要なら if TYPE_CHECKING: ブロックを使って、実行時の依存を増やさず型だけ提供。

“入口の一画面に、使い方の答えが置いてある”状態を目指すと、利用者体験が跳ね上がります。

🧱 便利パターン(小ワザ集)

  • 公開 API の再エクスポート:
    from .core import Foo, Bar; __all__ = ["Foo", "Bar"]

  • バージョンの一元化:
    手書きなら __version__ = "…", 配布後に読み出すなら from importlib.metadata import version as _v; __version__ = _v("pkg-name")

  • 軽いプラグイン自動登録:
    “パッケージを import したらエントリポイントを読み込む”程度は OK(ただし遅延可能なら遅延を検討)

  • ログ名前空間:
    import logging; logger = logging.getLogger(__name__)(上位で設定する前提の“薄い”取得)

py.typed は型配布の目印ファイル。__init__.py 本体ではなく、パッケージ直下に置いて mypy 等に「型情報を同梱してるよ」と伝えます。

🚫 アンチパターン(やらかし製造機)

  1. 初期化で重い処理

    • ネットワーク・ファイル I/O・DB 接続などを import 時 にやらない。CLI 起動遅延や REPL 体験が最悪になります。

  2. “全部盛り”再エクスポート

    • サブモジュールを片っ端から再エクスポートして巨大入口にする。循環依存・遅さ・衝突の温床。

  3. ワイルドカード輸入/輸出

    • from .core import * は読み手にもツールにも不親切。衝突や予期しない公開が起こりがち。

  4. 実行時状態を持つグローバルをここで作る

    • 変更を前提にした可変グローバル(コネクション・キャッシュ・設定オブジェクト)を入口で生成して配るのは危険。ライフサイクルを分離すべき。

  5. トップレベルでの副作用

    • print()・環境変数書き換え・作業ディレクトリ変更など、import しただけで世界が変わる行為は御法度。

  6. 回避不能な循環依存の温床化

    • __init__.py がアプリ全体へ依存を広げると、どこかで循環が爆発。入口は“参照される側”に寄せ、依存方向を一方通行に。

  7. 例外で落ちる入口

    • オプショナル依存が無いと import が落ちる実装は UX 最悪。「無ければ機能を無効化し、エラーメッセージは遅延表示」にするほうが親切。

“インポート=評価”という Python の性質を忘れがちです。__init__.py に書く=「誰かが import pkg するたび評価される」ことを常に意識。

🏗️ 大規模プロジェクトでの運用指針

  • 公開 API を“門番化”

    • サブパッケージで実装し、__init__.py では「何を見せるか」だけを決める。内部構造は自由に変えられる。

  • 安定面の方針をドキュメント化

    • 入口で公開した名前は SemVer に基づき安定保証、内部は非公開(破壊的変更可)…などルールを明文化。

  • 廃止(deprecation)は段階的に

    • 入口で古い名前を新しい場所へ薄くフォワードし、警告を一時的に出す。次メジャーで削除。

“入口で安定・内部は機敏”を徹底すると、利用者体験を守りながら内部改善を加速できます。

🔍 テストと品質のコツ

  • 入口の単体テストを用意

    • import pkg; dir(pkg) のスナップショット/公開名の存在確認テストは壊れやすい点の早期検知に効く。

  • インポート時間の監視

    • CI でインポート時間を測定し、閾値超過で警告を出すと“重くなりがち”を抑制できる。

  • 型チェックとの整合

    • 再エクスポート名に対して型チェッカが正しく解決できるか確認。__all__ と import 経路を一致させる。

「公開名リストのテスト」「インポート時間の監視」は、費用対効果が高いおすすめの二本柱です。

🧾 まとめチェックリスト(実務用)

  • 入口は“薄い”か?(重い処理・副作用なし)

  • 公開 API は明示されているか?(再エクスポート+__all__

  • 相対インポートで内部依存を整理しているか?

  • バージョンやメタデータはここから取得できるか?

  • オプショナル依存が無くても import は成功するか?

  • インポート時間は許容範囲か?(CI で監視)

  • 廃止予定 API に段階的移行策があるか?

__init__.py を“なんでも置き場”にすると、将来の自分が泣きます。入口は「最小で明快」。まずはここから。