同じ依存関係から uv と pip にそれぞれ pylock.toml を書き出させたら、16パッケージと15パッケージに分かれました。差分は colorama 1件。PEP 751 はツール非依存のロックファイル形式ですが、何を書き出すかまでは揃えていません。
requirements.txt が固定しきれなかったもの
pip freeze の出力に無い情報
pip freeze が書くのは名前とバージョンだけ。どのインデックスから取ったか、どの環境向けか、ファイルのハッシュがいくつか、どれも残りません。--require-hashes でハッシュは足せますが、その書式は pip 独自で、他のツールは解釈できない。
Poetry は poetry.lock、PDM は pdm.lock、uv は uv.lock。「依存を固定する」という同じ目的に対して、エコシステムごとに別形式が3つ以上並んでいました。CI のインストーラを差し替えると、ロックファイルごと作り直しになります。Go の go.sum もバージョン固定ではなくハッシュ台帳でした(Go Modulesの仕組みを実測する—go.sumがロックファイルではない理由)。
PEP 751 が決めたのはフォーマットだけ
PEP 751 は 2025年3月31日に Final になりました。決めたのはファイル形式で、依存解決のアルゴリズムには踏み込んでいません。仕様の “File Name” セクションはファイル名を pylock.toml、または正規表現 r"^pylock\.([^.]+)\.toml$" に一致する名前に限定します。pylock.ci.toml は有効、lock.toml は無効。
pylock.toml のトップレベルキーと [[packages]]
必須キーは2つ
packaging.python.org の pylock.toml 仕様が定義するトップレベルキーは次のとおりです。
| キー | 必須 | 役割 |
|---|---|---|
lock-version | 必須 | 現行は "1.0"。インストーラは互換性を確認する |
created-by | 必須 | 生成したツール名。"uv" / "pip" がそのまま入る |
requires-python | 任意 | 対応する Python バージョン指定子 |
environments | 任意 | サポートする環境マーカーの配列 |
extras / dependency-groups | 任意 | 選択可能な extras と依存グループ名 |
[[packages]] | 必須 | インストール候補になるパッケージの配列 |
配布物のテーブルは排他
1パッケージにつき [packages.vcs]、[packages.directory]、[packages.archive]、[[packages.wheels]] と [packages.sdist] のうち1系統しか書けません。インストーラは vcs → directory → archive → wheels → sdist の順で評価し、最初に見つかった系統を使います。
dependencies は解決に使ってはいけない
Tools MUST NOT use this information when doing installation; it is purely informational for auditing purposes.
仕様の [[packages.dependencies]] セクションにある一文。依存グラフを書き残すのは構わないが、インストーラはそれを読んで解決してはならない、という指定です。pylock.toml は解決済みの平坦なリストであって、依存グラフではない。だから [[packages]] の並び順にも意味がありません。
uv export と pip lock は同じ名前で別物を書く
検証環境は uv 0.11.25、pip 26.2.1、Python 3.14.6、macOS(arm64)。httpx==0.28.1 と rich==14.1.0 を直接依存に持つプロジェクトを /tmp/pylock-demo に作って比較しました。
uv export の出力
uv export --format pylock.toml -o pylock.toml
head -12 pylock.toml
# This file was autogenerated by uv via the following command:
# uv export --format pylock.toml -o pylock.toml
lock-version = "1.0"
created-by = "uv"
requires-python = ">=3.14"
[[packages]]
name = "anyio"
version = "4.15.0"
index = "https://pypi.org/simple"
sdist = { url = ".../anyio-4.15.0.tar.gz", upload-time = 2026-09-02T21:46:36Z, size = 276504, hashes = { sha256 = "b5c620ed..." } }
wheels = [{ url = ".../anyio-4.15.0-py3-none-any.whl", upload-time = 2026-09-02T21:46:35Z, size = 131908, hashes = { sha256 = "7ecd9937..." } }]
83行 / 7,660バイト、11パッケージ。index・size・upload-time まで残り、sdist と wheel の両方が記録されます。typing-extensions にだけ marker = "python_full_version < '3.15'" が付きました。
pip lock の出力
pip lock --index-url https://pypi.org/simple \
"httpx==0.28.1" "rich==14.1.0" -o pylock.pip.toml
WARNING: pip lock is currently an experimental command.
It may be removed/changed in a future release without prior warning.
生成物は123行 / 4,014バイト、同じく11パッケージ。行数は uv より多いのにバイト数は約半分です。uv がインラインテーブルで1行に詰めるのに対し、pip は [[packages.wheels]] を展開して書くため行が増える。そのうえで sdist・index・size・upload-time を一切書きません。
[[packages]]
name = "anyio"
version = "4.15.0"
[[packages.wheels]]
name = "anyio-4.15.0-py3-none-any.whl"
url = ".../anyio-4.15.0-py3-none-any.whl"
[packages.wheels.hashes]
sha256 = "7ecd9937369ffce8bba0b5ccb9b3a9507b101b0ed50256aecfbab27e6c2acb99"
差分は marker と sdist
pytest==8.4.2 を足すと差がはっきりします。uv は colorama を marker = "sys_platform == 'win32'" 付きで含め、pip は macOS で解決した結果しか書かないため colorama が消える。冒頭の16対15はこれです。
| 項目 | uv export –format pylock.toml | pip lock |
|---|---|---|
| パッケージ数(pytest込み) | 16 | 15 |
| 環境マーカー | 残す | 残さない |
| sdist | 記録する | 記録しない(wheelのみ) |
requires-python | >=3.14 | 書かない |
index / size / upload-time | あり | なし |
| 想定用途 | マルチプラットフォーム | 生成環境と同一プラットフォーム |
pip lock --help はこれを明示しています。
The generated lock file is only guaranteed to be valid for the current python version and platform.
dependency-groups キーはどちらのツールも書かない
PEP 751 のトップレベルには dependency-groups と default-groups があります。開発用依存を別枠で持ち、インストール時に選べる設計。ここが一番引っかかりました。
uv export –all-groups で pytest がフラットに混ざる
uv add --group dev "pytest==8.4.2" を実行してから書き出します。
uv export --format pylock.toml --all-groups -o pylock.groups.toml
python -c "
import tomllib, pathlib
d = tomllib.loads(pathlib.Path('pylock.groups.toml').read_text())
print('トップレベル:', list(d.keys()))
print('dependency-groups:', d.get('dependency-groups'))
print('pytest入り?:', any(p['name'] == 'pytest' for p in d['packages']))
"
トップレベル: ['lock-version', 'created-by', 'requires-python', 'packages']
dependency-groups: None
pytest入り?: True
pytest・pluggy・iniconfig は入っているのに、dependency-groups キーは出力されない。[[packages]] 側にもグループを示すマーカーが付きません。httpx と pytest が同じ平面に並び、区別する手がかりがゼロになります。
pip lock –group dev も同じ
pip lock --index-url https://pypi.org/simple --group dev -o pylock.pipgroup.toml .
トップレベル: ['lock-version', 'created-by', 'packages']
dependency-groups キー: None
pkgs: 16 # 'pytest' 'pluggy' 'iniconfig' を含む。'demo-app' 自身も混在
pip も --group を受け付けますが、結果は同じくフラット。ついでにローカルプロジェクト demo-app 自身が [packages.directory] として混ざります。
本番だけ入れる手段が残っていない
Docker のマルチステージビルドで、最終段に本番依存だけ入れる書き方が通りません。
# 想定どおり動かない書き方
COPY pylock.toml .
RUN pip install -r pylock.toml # pytest ごと入る
グループを分けたいなら、書き出しの時点で分割しておくしかありません。仕様がファイル名に pylock.*.toml を許しているので、用途ごとに複数置けます。
uv export --format pylock.toml --no-dev -o pylock.prod.toml
uv export --format pylock.toml --all-groups -o pylock.dev.toml
1ファイルで両方を賄う設計は、生成側が dependency-groups を書くようになるまで成立しません。Pythonのパッケージ管理はuvで統一すべき?pip・poetryとの違いで触れた uv.lock はグループを保持したままなので、uv sync --no-dev は今も普通に動きます。捨てられない理由がここ。
pylock.toml からインストールする手順
pip 26.1 以降の -r pylock.toml
pip は 25.1 で pip lock、26.1 で pip install -r pylock.toml を追加しました。どちらも experimental 扱いのままです。uv が書いたファイルを pip に食わせてみます。
pip --python /tmp/v_uv/bin/python install -r pylock.toml
WARNING: Using pylock.toml as a requirements source is an experimental feature.
It may be removed/changed in a future release without prior warning.
Successfully installed anyio-4.15.0 certifi-2026.7.22 h11-0.16.0 httpcore-1.0.9
httpx-0.28.1 idna-3.19 markdown-it-py-4.2.0 mdurl-0.1.2 pygments-2.21.0
rich-14.1.0 typing-extensions-4.16.0
警告は出るものの、他ツールが生成したファイルがそのまま通りました。uv pip sync pylock.toml も同じ11パッケージで完了します。相互運用は成立している。
marker はインストール時に評価される
colorama 入りの16パッケージ版を macOS で入れると、導入されるのは15個。
導入数: 15
['Pygments', 'anyio', 'certifi', 'h11', 'httpcore', 'httpx', 'idna',
'iniconfig', 'markdown-it-py', 'mdurl', 'packaging', 'pluggy', 'pytest',
'rich', 'typing_extensions'] # colorama は入らない
仕様の “Installation” セクションが定めるインストーラの手順は、lock-version の確認 → requires-python の照合 → marker の評価 → 配布物の選択、という並びです。sys_platform == 'win32' が偽になり、colorama は候補から外れる。逆に言えば、marker を持たない pip 生成のファイルは Windows 用の情報を最初から持ちません。
ハッシュを1文字変えると止まる
httpx の wheel ハッシュの先頭4文字を 0000 に書き換えて、キャッシュを無効にしてインストールします。
pip --python /tmp/v_tamper/bin/python install --no-cache-dir -r pylock.tampered.toml
ERROR: THESE PACKAGES DO NOT MATCH THE HASHES FROM THE REQUIREMENTS FILE.
If you have updated the package versions, please update the hashes.
Otherwise, examine the package contents carefully; someone may have tampered with them.
httpx==0.28.1 from .../httpx-0.28.1-py3-none-any.whl:
Expected sha256 0000fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad
Got d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad
PEP 751 がインストーラにハッシュ検証を義務づけているため、--require-hashes は要りません。pylock.toml を渡した時点で pip が検証します。
2026年9月時点の対応ツール
| ツール | 生成 | インストール | 備考 |
|---|---|---|---|
| uv | uv export --format pylock.toml | uv pip sync / uv pip install -r | 0.6.15 で先行対応。主形式は uv.lock のまま |
| pip | pip lock(25.1〜, experimental) | -r pylock.toml(26.1〜, experimental) | 生成物は生成環境専用 |
| PDM | 対応 | 対応 | ネイティブ形式と併存 |
| Pipenv | 対応 | 対応 | 2026系で提供 |
| Poetry | 未対応 | 未対応 | issue #10356 で追跡中 |
サジェストの “pep 751 poetry” の答えは、2026年9月時点では未対応。poetry.lock を置き換えるか export を先に出すかで議論が続いています。
まとめ
- PEP 751 は2025年3月31日に Final。
lock-versionは"1.0"、必須キーはlock-versionとcreated-byと[[packages]] uv exportは marker と sdist を残すマルチプラットフォーム向け、pip lockは生成環境専用。同じ依存で16対15に割れるdependency-groupsキーは uv も pip も書き出さない。本番/開発を分けるならpylock.prod.tomlのように書き出し側で分割する
相互運用そのものは動きます。uv が書いたファイルを pip がハッシュ検証つきで入れられて、marker も正しく評価される。ただ「1ファイルで全部賄う」段階には届いていないので、当面は uv.lock を主、pylock.toml を配布用のエクスポートとして扱うのが現実的です。