Pythonで書いたCLIツールは、--helpを出すだけでも全モジュールのimportを実行します。手元の計測では、起動32.9msのうち約24msがimport由来。Python 3.15のlazy import(PEP 810)は、この読み込みを「初めて使う瞬間」まで遅らせる新構文です。
起動32.9msの内訳をimporttimeで確認する
検証に使ったのは、asyncio・email.mime.multipart・http.client・decimal・sqlite3の5モジュールをimportするCLI想定のスクリプト。引数なしで実行するとusageを表示して終わる、どのモジュールも使わないパスです。
python3.15 -X importtime cli.py 2>&1 | sort -t'|' -k2 -nr | head -3
import time: 134 | 14861 | asyncio
import time: 406 | 13126 | asyncio.base_events
import time: 73 | 4341 | email.mime.multipart
-X importtimeで内訳を出す
asyncioだけで14.9ms。右列が累積マイクロ秒で、asyncio.base_eventsやloggingを芋づる式に引き込んでいるのが読み取れます。usageを表示するだけの実行でも、このコストは毎回発生します。
効くのはCLI・テスト・サーバーレス
PEP 810は “Command-line tools are often invoked directly by a user, so latency – in particular startup latency – is quite noticeable.” と、CLIの起動遅延を筆頭の動機に挙げています。1回あたり数十msでも、Typerで作るCLIのようにサブコマンドが増えるほどimportは積み上がる。テストスイートの起動や、コールドスタートが課金に直結するサーバーレスも同じ構図です。
lazyキーワードの書き方
lazy import json
lazy from pathlib import Path
import文の先頭にlazyを付けるだけ。lazyはソフトキーワードなので、既存コードでlazyという変数名を使っていても壊れません。対象はPython 3.15以降で、2026年7月時点ではベータ版(3.15.0b4)。正式リリースは2026年10月1日予定です。
lazy importとlazy from
どちらの形式も書けます。禁止されているのはlazy from x import *とlazy from __future__ importの2形式。ワイルドカードはモジュールを実行するまで束縛する名前が決まらず、名前が未確定のままではプロキシを作れません。
書けるのはモジュールレベルだけ
関数内やtryブロック内に書くと構文エラーになります。
def load():
lazy import json
SyntaxError: lazy import not allowed inside functions
try:
lazy import ujson
except ImportError:
import json
SyntaxError: lazy import not allowed inside try/except blocks
tryで代替パッケージにフォールバックする定番パターンが書けないのは、例外の出るタイミングが後述の通り「初回アクセス時」に移るから。文の時点で例外を捕まえる書き方とは両立しません。
実測: 5モジュールのCLIで73%短縮
計測条件
Python 3.15.0b3(macOS/Apple Silicon)で、冒頭の5モジュール構成スクリプトを引数なし(モジュール未使用パス)で20回実行し、中央値を取りました。比較したのは通常のimport、lazy構文、-X lazy_imports=allの3条件です。
import sys
import asyncio
import email.mime.multipart
import http.client
import decimal
import sqlite3
def main():
if len(sys.argv) < 2:
print("usage: tool <command>")
return
# サブコマンド処理でのみ上記モジュールを使う
main()
lazy版は、この5行のimportにlazyを付けただけ。ロジックは一切変えていません。
eager比27%まで縮んだ
| 条件 | 起動時間(中央値) | eager比 |
|---|---|---|
| eager(通常のimport) | 32.9ms | 100% |
| lazy構文(5行書き換え) | 8.8ms | 27% |
-X lazy_imports=all(書き換えなし) | 8.3ms | 25% |
32.9ms → 8.8msで73%の短縮。PEP 810に載っている実績値 “This can reduce startup time by 50-70% in practice” と整合します。メモリについても “Memory savings of 30-40% have been observed in real workloads.” という報告つき。ただしインタプリタ自体の起動(約8ms)が下限になるため、削れるのはあくまでimport部分です。importしたモジュールを必ず全部使うスクリプトでは、コストが後ろにずれるだけで合計は変わりません。
プロキシが実体化するタイミング
sys.modulesで観察する
import sys
lazy import json
print('json' in sys.modules)
print(json.dumps({"k": 1}))
print('json' in sys.modules)
False
{"k": 1}
True
lazy importの時点では名前空間に遅延プロキシが置かれるだけで、sys.modulesにはまだ載りません。初回アクセスで本物のモジュールに置き換わる。この置き換えをWhat’s New in Python 3.15の “PEP 810: Explicit lazy imports” セクションではreificationと呼び、プロキシの型としてtypes.LazyImportTypeが追加されています。現在のモードはsys.get_lazy_imports()で確認でき、既定値はnormalです。
from importは1つ触ると全体が入る
import sys
lazy from json import dumps, loads
dumps({})
print('json' in sys.modules) # True
名前ごとにプロキシが作られますが、どれか1つにアクセスした時点でモジュール全体が実行されます。loadsだけ遅延し続ける、という粒度にはなりません。
ImportErrorは初回アクセスまで飛ばない
lazy import nonexistent_module # この行は素通りする
print("import文は通過した")
nonexistent_module.foo() # ここで初めて例外
import文は通過した
Traceback (most recent call last):
...
ImportError: lazy import of 'nonexistent_module' raised an exception during resolution
依存の入れ忘れやモジュール名のtypoが、import文ではなく使用箇所で発覚します。起動直後に落ちてほしいバッチや常駐プロセスでは、この遅延が裏目に出る。一方で元の例外はchainされて出るため、原因の特定自体は難しくありません。
ただし、import時の副作用(ロガー登録・プラグイン登録)に依存するモジュールをlazyにすると実行順が変わります。lazy化の前提は、importされるだけで何かが起きるコードがないことです。
コードを書き換えずに一括で効かせる
モードはnormal(lazy構文のみ遅延)・all(モジュールレベルのimportをすべて遅延扱い)・none(すべて即時)。指定経路は次の通りです。
| 経路 | 指定方法 | 想定場面 |
|---|---|---|
| CLIオプション | python -X lazy_imports=all app.py | 書き換えなしで効果を試す |
| 環境変数 | PYTHON_LAZY_IMPORTS=all | コンテナ・CIでプロセス全体に適用 |
| API | sys.set_lazy_imports("all") | アプリの起動コードで恒久化 |
sys.set_lazy_imports_filter()を併用すると対象を絞れます。手元でasyncioとdecimalだけを対象にするフィルタを設定したところ、対象外のjsonは従来通り即ロードのまま。ライブラリ作者向けには、モジュール変数__lazy_modules__ = ["重いモジュール名"]で通常のimport文をlazy扱いにする経路もあります。
循環インポートの回避にも効く
from import型の循環が通る
# a.py
from b import world
def hello():
return "a.hello"
def use_b():
return world()
# b.py
from a import hello
def world():
return hello() + " -> b.world"
ImportError: cannot import name 'hello' from 'a'
b.py側をlazy from a import helloに変えるだけで、この循環はa.hello -> b.worldと正常に動きました。モジュール初期化中に相手の名前を解決しに行かなくなるためです。
依存の癒着を隠す面もある
PEP 810のFAQには “Do lazy imports work with circular imports?” という項目があり、答えは条件付きです。動くのは初期化中にアクセスしない場合だけで、循環そのものはそのまま。lazyで通した循環は、依存を切る改修が終わるまでのつなぎに留めます。
Python 3.14以前でやるなら
標準ライブラリのimportlib.util.LazyLoaderが従来の遅延手段です。ローダーを組む準備コードが10行前後、PEP 810は先頭の1語。準備コード10行を数百箇所のimportには貼れませんが、1語なら機械的な置換で済みます。しかもLazyLoaderはfrom ... importに効きません。
Python 3.14のサブインタプリタやfree-threadingが実行時の並列性を攻めたのに対し、3.15は起動時間に踏み込みました。試すだけならuv python install 3.15で、手元では3.40秒でベータ版が入りました。
まとめ
- lazy importはモジュールの読み込みを初回アクセスまで遅らせる。Python 3.15(2026年10月1日正式リリース予定)の新構文
- 5モジュール構成のCLI実測で起動32.9ms → 8.8ms(73%短縮)。
-X lazy_imports=allなら書き換えなしで試せる - 書けるのはモジュールレベルだけ。関数内・tryブロック内はSyntaxError
- ImportErrorは初回アクセス時に飛ぶ。依存の入れ忘れの発覚が実行時まで遅れる
- from import型の循環インポートを回避できるが、PEP 810のFAQいわく条件付き。設計の見直しの代わりにはならない
- 3.14以前の代替は
importlib.util.LazyLoader。from importに効かない制約つき

