__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
を作るほうが現実的です。
✅ ベストプラクティス
-
薄く保つ(Heavy is bad)
初期化は“軽い”範囲に限定。時間のかかる import・I/O・ネットワークアクセスは厳禁。 -
公開 API を明示する
-
再エクスポートする場合は明示的に書く:
例)from .core import Foo, Bar
-
さらに
__all__ = ["Foo", "Bar"]
で補完と静的解析を助ける。
-
-
相対インポートを使う
-
パッケージ内は
from .submodule import Foo
のように相対で。移動やリネームに強く、循環も気づきやすい。
-
-
メタデータをここに集約
-
__version__ = "1.2.3"
のように、利用者がimport pkg; pkg.__version__
で確認できるようにする。 -
バージョンは、ビルド時に書き換える・
importlib.metadata
から読む、どちらに寄せるかをチームで統一。
-
-
遅延インポート(必要なら)
-
コールドスタートが重い場合、関数内で import して遅延させる。
-
ただしやり過ぎは可読性低下。入口での“重い再エクスポート”をやめるだけでも大抵は改善します。
-
-
ドキュメントストリングを入口に
-
パッケージ docstring に「このパッケージの何が嬉しいか」「主要コンポーネント」を簡潔に記すと親切。
-
-
型の見え方を整える
-
再エクスポートで “外から見える型” を整理しておくと、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 等に「型情報を同梱してるよ」と伝えます。
🚫 アンチパターン(やらかし製造機)
-
初期化で重い処理
-
ネットワーク・ファイル I/O・DB 接続などを import 時 にやらない。CLI 起動遅延や REPL 体験が最悪になります。
-
-
“全部盛り”再エクスポート
-
サブモジュールを片っ端から再エクスポートして巨大入口にする。循環依存・遅さ・衝突の温床。
-
-
ワイルドカード輸入/輸出
-
from .core import *
は読み手にもツールにも不親切。衝突や予期しない公開が起こりがち。
-
-
実行時状態を持つグローバルをここで作る
-
変更を前提にした可変グローバル(コネクション・キャッシュ・設定オブジェクト)を入口で生成して配るのは危険。ライフサイクルを分離すべき。
-
-
トップレベルでの副作用
-
print()
・環境変数書き換え・作業ディレクトリ変更など、import しただけで世界が変わる行為は御法度。
-
-
回避不能な循環依存の温床化
-
__init__.py
がアプリ全体へ依存を広げると、どこかで循環が爆発。入口は“参照される側”に寄せ、依存方向を一方通行に。
-
-
例外で落ちる入口
-
オプショナル依存が無いと 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
を“なんでも置き場”にすると、将来の自分が泣きます。入口は「最小で明快」。まずはここから。