PyInstaller のビルド成功は動作保証ではない ― 遅延importが生む実行時クラッシュと、出荷前に止める3つの改札
投稿日:2026.08.20
CATEGORY
- 製造業DX
「ビルドが通ったから大丈夫」と判断して出荷した exe が、現場でQRコードを一枚も生成できず全数不良になったことがあります。原因は、PyInstaller の静的解析が実行時にしか踏まれない import を検出できなかったことでした。PyInstaller が ModuleNotFoundError を出さずにビルドを通しても、それは「必要な依存が全部揃っている」ことの証明にはなりません。関数の中に隠れた import が1つあるだけで、失敗は実行時まで静かに先送りされます。
PyInstaller の静的解析が届く範囲
PyInstaller は、スクリプトを実際に実行するのではなく、ソースコードを読んで import 文を辿ることで同梱すべきモジュールを決めています。この解析方法には、届く範囲と届かない範囲があります。
関数やメソッドの中に書かれた import(遅延 import)は、その関数が呼ばれるまで実行されないコードです。PyInstaller は解析のためにコードを実行しているわけではないので、関数の中にしか出てこない import を確実には辿れません。依存が venv に無くても、ビルドは成功し、警告も出ません。同じ理由で、たとえ依存が venv にあっても、遅延 import された分は解析の対象から漏れ、exe に同梱されないことがあります。

一方、モジュールの先頭に書かれた import は、ファイルを読み込む時点で解析器が辿れるため、少なくとも解析器の視野には入ります。ただし、これは「安全」を意味しません。モジュール先頭に書かれた 3rd-party import であっても、venv に無ければ「ビルド成功・起動即クラッシュ」になるケースを、私たちは別のツールの matplotlib で経験しています。「先頭に書けばビルドが止めてくれる」という期待自体が誤りで、解析器が import の存在を把握できることと、依存が実際に揃っているかどうかの検証は別問題です。
全数QR生成失敗という現場の被害
2026年7月、私たちが保守する、大手自動車メーカーの製造ライン向けツールでこの死角が現実になりました。QRコード生成ライブラリの qrcode が、Pillow(PIL)を関数内で遅延 import していたのです。
ビルド環境の venv には Pillow が入っていませんでした。それでもビルドは成功し、PyInstaller は何の警告も出しませんでした。現場に出た exe は起動もします。問題は、QRコードを生成する処理に入って初めて表面化しました。全数で QR 生成が失敗し、ライン影響が発生しました。結局、旧バージョンへの切り戻しで凌ぐことになりました。

この一件が突きつけたのは、「ビルドが成功した」という事実は「exe が現場の全処理を最後まで実行できる」ことの証明にはならない、という単純な事実です。ビルドログの緑色の成功表示は、静的解析が辿れた範囲でのチェックが通っただけであり、遅延 import の先にある処理は、まだ一度も検証されていません。
出荷前に止める3つの改札
この経験から、私たちは exe を出荷する前に通す3つの改札を、複数のリポジトリで運用しています。
改札1: ビルド前 import ゲート。ビルドスクリプトの冒頭で python -c "import PIL, PyQt5, cv2, ..." のように、そのプロダクトが実際に使っている 3rd-party モジュールを import してみて、1つでも失敗したらビルド自体を止めます。これは venv 側の欠落を出荷前に検出するための改札で、import リストは各リポジトリの実際の import と同期させておく必要があります。ただし、この改札だけでは足りません。関数内の遅延 import は venv に Pillow があっても、PyInstaller がそれを bundle に含めるとは限らないためです。
改札2: --hidden-import の明示と --clean の恒久付与。遅延 import された依存(qrcode.image.pil や PIL.Image など)は、PyInstaller の --hidden-import オプションで明示的に伝える必要があります。加えて、PyInstaller の解析結果はキャッシュされるため、venv にモジュールを追加した後もキャッシュが古いままだと、欠落した exe を再生産してしまいます。--clean を付けるとビルドのたびに解析をやり直すため多少時間はかかりますが、私たちは欠落 exe を出さないことを優先し、この2つを恒久フラグとして固定しています。ただし、--hidden-import の指定漏れ自体は、フラグを付けただけでは気づけません。指定した通りに動くかを確かめるのが、次の改札の役目です。
改札3: 本番相当の設定ファイルを置いたスモークテスト。ビルド直後に本番相当の config(ini)ファイルを exe と同じフォルダに置いた状態で起動し、動作確認をします。config を置かずにスモークテストをすると、設定読み込みエラーで処理が止まり、QR生成などの本処理に到達しないまま「エラーは出たが動いてはいる」ように見えてしまいます。実際に、私たちの現場でも config なしのスモークがエラーを大量に出しながら QR 処理には到達せず、Pillow 欠落を見逃したまま出荷してしまった経験があります。この改札で確認しているのは3点です。起動後のログに想定外の ERROR が出ていないこと、QR.png のような機能生成物のタイムスタンプが起動時刻に更新されていること、ログの増加速度が異常でないことです。
まとめ
あなたのビルドスクリプトでまず確認できるのは、--hidden-import のリストが実際の遅延 import 先と一致しているか、そして --clean が恒久で付いているかどうかの2点です。前者がずれていれば同梱漏れがそのまま残り、後者が抜けていれば直近のビルドが古い解析キャッシュの上に成り立っている可能性があります。
PyInstaller のビルド成功は、依存関係のうち解析器が辿れた範囲を通過しただけの結果です。関数の中に隠れた import がある限り、その先の処理は現場で最初に実行されるまで未検証のままです。3つの改札は、その未検証の範囲を出荷前に減らすための仕組みであり、1人の注意力に頼らずに繰り返し機能します。
出典:
※ この記事は AI を使用しています。Leadeas が自社開発した AI エージェント基盤で下書きを作成し、人間のレビューを経て公開しています。AI ネイティブ開発会社として自社の技術をそのまま実演する目的で、この手法を用いています。(詳しくは AI 利用ポリシー)

