Pythonの.pycはいつ作り直されるのか:timestampとハッシュベースの無効化
CPythonは既定で、ソースの更新時刻とサイズを見て.pycが最新かどうかを判断します。この方式が変更を見逃す場合、checked-hashとunchecked-hashの違い、compileallの使い方をPython 3.14.8のドキュメントに基づいて整理しました。
Pythonのソースを書き換えたのに、実行結果が変わらない。__pycache__を消して直ったなら、古いキャッシュが原因だったことまでは分かります。ただ、なぜそのキャッシュが使われたのかは分かりません。CPythonが.pycを信頼してよいと判断する方法は3つあり、どの方式で作られたかによって判断基準が異なります。
.pycはimportのキャッシュ
CPythonはimportの際にソースモジュールをコンパイルする必要があると、そのバイトコードを.pycファイルに保存し、後の読み込みで再利用できるようにします。Python 3.2でPEP 3147が導入されてから、このファイルはソースと同じ場所の__pycache__ディレクトリに置かれ、ファイル名にインタープリターのタグが付きます。CPython 3.14なら次のような形です。
pkg/
├── settings.py
└── __pycache__/
└── settings.cpython-314.pyc
タグはsys.implementation.cache_tagから来ます。ドキュメントによると、慣例では実装名とバージョンを組み合わせた値('cpython-33'のような形)ですが、実装によっては別の値を使うこともあります。タグを分けた理由はPython 3.2の「What’s New」に書かれています。以前は複数のインタープリターが同じmymodule.pycを取り合い、互いに再コンパイルして上書きしていました。そのため、.pycを別のPythonバージョンや別の実装に持ち込めるファイルとして扱うべきではありません。
あるソースに対応するキャッシュのパスは、標準ライブラリで確認できます。
import importlib.util
print(importlib.util.cache_from_source("pkg/settings.py"))
-Bオプションや環境変数PYTHONDONTWRITEBYTECODEを使うと、import時に.pycを新たに書き込みません。PYTHONPYCACHEPREFIX(または-X pycache_prefix=PATH、3.8以降)を指定すると、__pycache__ではなく指定したパスの下にミラーしたツリーへ書き込みます。
3つの無効化方式
Python 3.7のPEP 552で.pycの形式が拡張され、ソースのハッシュで無効化を判断できるようになりました。それまではtimestamp方式しかありませんでした。どの方式で作った.pycかはファイルのヘッダーに記録されます。次の表はPython 3.14.8のドキュメントに書かれている現在の動作です。
| 方式 | .pycに記録されるもの |
import時の処理 |
|---|---|---|
| timestamp(既定) | ソースの最終更新時刻とサイズ | 現在のソースの更新時刻・サイズと比較 |
| checked-hash | ソース内容のハッシュ | ソースをハッシュして比較し、違えば作り直して書き込む |
| unchecked-hash | ソース内容のハッシュ | ファイルがあれば有効とみなし、ソースとは比較しない |
ヘッダーに入るハッシュは、importlib.util.source_hash()がそのソースに対して返す値です。3.7の「What’s New」によると、CPythonは既定では引き続きtimestamp方式を使い、import中にハッシュベースの.pycを生成しません。ハッシュベースの.pycはpy_compileかcompileallで作ります。
timestamp方式が見逃すケース
timestamp方式は、比較のためにソースの中身を読みません。記録された更新時刻とサイズが現在のソースと一致すれば、中身が変わっていてもキャッシュをそのまま使います。
たとえばsettings.pyのVALUE = 1をVALUE = 2に変えても、ファイルサイズは同じです。更新時刻まで以前の値と一致してしまうのは、次のような場合です。
- ファイルシステムのタイムスタンプの分解能が粗く、同じ時刻の単位の中で2回編集した場合。3.7の「What’s New」にも、タイムスタンプが粗すぎるとソースの更新を見逃すことがあると書かれています。
- デプロイや展開のツールが元ファイルの更新時刻を保ったままコピーし、配置先に、同じ時刻とサイズで別の内容から作られた
.pycがすでに残っている場合。 - ビルドの過程ですべてのファイルの更新時刻を固定値にそろえる場合。
これらの条件が重なると、importはエラーを出さずに古いバイトコードを実行します。この記事のために上の状況を実際に試してはおらず、ドキュメントに書かれた比較のルールから導かれる結果として説明しています。
逆に、中身は同じで更新時刻だけが変わった場合は再コンパイルされます。結果が間違うことはありませんが、コンテナのビルドのように毎回更新時刻が変わる環境ではキャッシュが再利用されにくくなります。
checked-hashとunchecked-hash
checked-hashの.pycからモジュールを読み込むとき、Pythonはソースファイルを読んでハッシュを計算し、ヘッダーの値と比較します。すでにsys.modulesにあるモジュールはこの検査なしでそのまま返されるので、検査が行われるのはimport文を実行するたびではなく、モジュールを読み込むときです。ハッシュが違っていれば再コンパイルし、新しいchecked-hashの.pycを書き込みます。更新時刻に依存しないので、前の節の状況でも中身の変更に気づきます。その代わり、ソース全体を読んでハッシュするコストがかかります。
unchecked-hashの.pycは、ファイルが存在すれば有効とみなされます。ソースを書き換えても、.pycを作り直すまでは古いバイトコードが実行されます。py_compileのドキュメントは、この方式はビルドシステムのようなPython外部の仕組みが.pycを最新に保つ場合に役立つと説明しています。ソースを直接編集する開発環境には向きません。
ハッシュベースの.pycの検証動作は、インタープリターのオプションで変えられます。
python --check-hash-based-pycs always app.py
| 値 | 動作 |
|---|---|
default |
checkedは検証し、uncheckedは検証しない |
always |
checkedもuncheckedもソースと比較する |
never |
ハッシュベースの.pycをソースと比較しない |
このオプションはtimestamp方式の.pycには影響しません。デプロイ先でunchecked-hashの.pycが疑わしいときは、alwaysで一度実行して結果が変わるかを見ると、キャッシュが原因かどうかを切り分ける手がかりになります。
無効化方式を指定してコンパイルする
compileallはディレクトリを再帰的にたどってコンパイルします。
python -m compileall --invalidation-mode checked-hash src/
python -m compileall -f -q --invalidation-mode unchecked-hash build/lib/
--invalidation-modeにはtimestamp、checked-hash、unchecked-hashを指定できます。-fはタイムスタンプが最新でも再コンパイルします。-qはコンパイルしたファイルの一覧を表示しません。エラーメッセージは残り、-qqならすべて抑止します。
ファイル1つならpy_compileで作れます。
import py_compile
py_compile.compile(
"src/settings.py",
invalidation_mode=py_compile.PycInvalidationMode.CHECKED_HASH,
)
既定値は環境変数SOURCE_DATE_EPOCHによって変わります。py_compile.compile()もcompileallも、この変数が設定されていれば既定値はchecked-hash、設定されていなければtimestampです。py_compileは3.7ではこの変数があると指定したモードをchecked-hashで上書きしていましたが、3.7.2からは既定値を決めるだけになりました。意図しない方式で.pycが作られていたら、ビルド環境でこの変数が設定されていないかをまず確認するとよいでしょう。
__pycache__の削除は切り分けの手順
__pycache__を消して問題が消えたなら、確認できたのは古いキャッシュが原因だったということまでです。再発を防ぐには原因を探す必要があります。
- デプロイツールが更新時刻を保持または固定していないか
- ビルド成果物に含まれる
.pycがどの無効化方式で作られたか - unchecked-hashの
.pycを使いながら、ソースだけを差し替える経路がないか - ビルド環境に
SOURCE_DATE_EPOCHが設定されていないか
どの方式を選ぶか
- ローカル開発や通常の実行:既定のtimestampで十分です。
- 更新時刻が信頼できない場合や、再現可能なビルドが必要な場合:checked-hash。3.7の「What’s New」も、キャッシュにタイムスタンプが入っているとビルドの再現性や内容ベースのビルドシステムで問題になると説明しています。
- ソースと
.pycが常に同じビルドでまとめて差し替えられ、デプロイ後にソースを直接編集しない場合:unchecked-hash。ソースを個別にパッチする可能性があるなら避けたほうが無難です。
ハッシュベースの.pycは、キャッシュが最新かどうかを判断する方法にすぎません。コード署名ではなく、ファイルが改ざんされていないことも保証しません。
この記事の動作は、2026年10月9日にPython 3.14.8のドキュメントで確認しました。
参考にしたドキュメント
- Python 3.14.8 言語リファレンス:Cached bytecode invalidation
- py_compile、compileall、importlib、sys.implementation
- コマンドラインと環境:
--check-hash-based-pycs、-B、PYTHONPYCACHEPREFIX - What’s New:Python 3.7(PEP 552)、Python 3.2(PEP 3147)