PEP 751 stopped being a proposal a while ago. It was accepted on March 31, 2025, its status is Final, and the living text now lives in the PyPA packaging specifications rather than in the PEP itself. What it standardized is narrower than most summaries admit: how an already-computed resolution is written to disk — not how you compute it, and not how you update it afterwards. The practical consequence lands immediately. A pylock.toml produced by pip and one produced by uv are both perfectly valid, carry the same filename, and are not the same kind of artifact: one only installs on the machine that generated it, the other is portable across platforms — and nothing in the file tells a consumer which one it is holding.

That gap is the whole story of adopting this format in 2026. Everything below is checked against the spec text, the actual changelogs, and files I generated locally with pip 26.2.1 and uv 0.12.2.

What the format actually is

Three keys are required: lock-version (currently "1.0" is the only legal value), created-by, and a [[packages]] array. Optional top-level keys are environments, requires-python, extras, dependency-groups, default-groups, plus a [tool] table for vendor extensions. The filename must match ^pylock\.([^.]+\.)?toml$ — so pylock.toml and pylock.dev.toml are fine, while pylock..toml and pylock.foo.bar.toml are rejected. That naming rule is deliberate: a service can look for pylock.<its-own-name>.toml first and fall back to pylock.toml.

Two normative rules define the format's personality more than the schema does.

packages.dependencies is informational only. The spec is explicit that tools "MUST NOT use this information when doing installation; it is purely informational for auditing purposes." So a pylock.toml is not a dependency graph waiting to be walked. It is a flat, pre-computed manifest. The installer's job is reduced to filtering: check lock-version, check requires-python, evaluate each packages.marker, pick exactly one source per package (one of wheels, sdist, vcs, directory, archive), install. No resolver runs at install time. That is the entire point of the format, and it's the property that makes it worth adopting for deployment.

A package may appear multiple times, but must narrow to a single entry at install time. This is how multi-environment locking works: one entry with marker = "sys_platform == 'win32'", another with marker = "sys_platform != 'win32'". Hashes are mandatory — an artifact's hashes table MUST have at least one entry, and SHOULD include a secure algorithm from hashlib.algorithms_guaranteed. Any conforming pylock.toml is therefore verifiable by construction, which matters more than it sounds: pip automatically enables --require-hashes when it encounters requirements with hashes (pip 26.2 added --no-require-hashes to opt out of that).

What it deliberately does not do

Brett Cannon's retrospective from October 10, 2025 is worth reading for the archaeology. Four years, one abandoned predecessor (PEP 665), and the central fight was single-use versus multi-use lock files, with Astral, Poetry, and PDM holding incompatible positions. The compromise was to allow both shapes and let each tool decide — which is exactly why the format is easy to emit and hard to consume.

Three holes follow from that, and each one bites in production.

There is no path back to a lock. A pylock.toml records the resolution, not the constraints that produced it. You cannot start from a pylock.toml and say "bump requests, leave everything else pinned," because the file never recorded what you actually asked for. No tool accepts a pylock.toml as re-locking input, and none can, because the information simply isn't in the file. It is an output format, full stop.

Single-use and multi-use are indistinguishable. The absence of an environments key means "no environment constraint declared" — not "this file is single-environment." A file valid only on macOS/CPython 3.14 and a file covering every platform are structurally legal siblings.

Dependency-group boundaries get flattened. The spec anticipated this: extras and dependency-groups exist as top-level keys, and marker syntax was extended with two new set-valued variables. Verified with packaging 26.3:

from packaging.markers import Marker
Marker("'dev' in dependency_groups")   # OK
Marker("'test' in extras")             # OK

Support for parsing those landed in packaging 25.0 (2025-04-19). But neither uv nor pip writes them on export today, so the door the spec opened is still unused.

Two tools, one filename

This isn't inference. Here is a minimal project — click>=8.1, uvloop>=0.19; sys_platform != 'win32', and a dev group containing pytest — locked two ways on macOS/arm64 with CPython 3.14:

python -m pip lock "click>=8.1" "uvloop>=0.19; sys_platform != 'win32'" -o pylock.toml
uv export --format pylock.toml -o pylock.toml

The first snapshots the resolution for the interpreter and platform running the command. The second translates the project's existing uv.lock.

pip's file has exactly two packages, click and uvloop, each with exactly one wheel (uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl), no marker on either, no requires-python, and only three top-level keys. Note what is missing: colorama. It's click's Windows-only dependency; its marker evaluated false at lock time, so the entry was dropped entirely.

uv's file has requires-python = ">=3.11", keeps colorama with marker = "sys_platform == 'win32'", carries 30 uvloop wheels spanning cp311–cp314 across macOS, manylinux and musllinux (free-threaded builds add yet another set of t-suffixed ABI tags on top of that — see Python Free-Threading in Production), and records size and upload-time on every artifact.

Both files are valid PEP 751. Both are named pylock.toml.

The loud failure and the quiet one

Feed pip's file to a CPython 3.12 environment and uv refuses cleanly (interpreter upgrades are where this class of ABI break shows up most often — see Python 3.15 beta4 migration guide):

error: Package `uvloop` can't be installed because it doesn't have a source
distribution or wheel for the current platform

hint: You're using CPython 3.12 (`cp312`), but `uvloop` (v0.22.1) only has
wheels with the following Python implementation tag: `cp314`

That's the good case: the record exists, the artifact doesn't match, you get told. The dangerous case is colorama. On Windows, that same file installs cleanly, exits 0, and click's colored output breaks at runtime. The installer never consults packages.dependencies, so it has no mechanism to notice that something was pruned. A missing package is not a detectable error in this format — it's just a shorter list.

pip knows where the edge is and refuses to pretend otherwise:

ERROR: Platform and interpreter constraints using --python-version, --platform,
--abi, or --implementation, are not supported when selecting requirements from
'pylock.toml'

One more detail that's easy to miss. pip 26.2 added support for the upload-time field so that --uploaded-prior-to — the dependency-cooldown control that exists to blunt supply-chain attacks on freshly published versions — works with -r pylock.toml. But in my test, pip's own pip lock output carried neither size nor upload-time. The cooldown flag is only useful against locks produced by a tool that records upload times, which today means uv, not pip.

environments and marker convergence

If you generate pylock.toml yourself — from an internal resolver, an SBOM pipeline, a Bazel rule — these are the two fields to get right.

environments is an array of marker expressions declaring which environments the lock was built for. The installer algorithm requires verifying that at least one of them is satisfied, or erroring out. It is the one explicit guardrail the format offers against the portability problem described above. Neither pip nor uv writes it today, so the guardrail is empty in practice. Write it. It's the only mechanism that converts "this lock doesn't apply to your machine" from a runtime mystery into an install-time hard failure.

packages[].marker controls when each entry applies, and the convergence rule is strict: after evaluation, every package to be installed must resolve to exactly one entry. Two records for the same package that are both true in the same environment is a conflict the installer must report. This is the most common way hand-assembled or merged lock files go wrong; pip 26.2 shipped "better error messages in case of conflicts with requirements from -r pylock.toml," which tells you how often it happens in the field.

Tool support, as of August 2026

pip got there in two widely separated steps. pip lock arrived in 25.1 (2025-04-26) as an experimental command. Installing from a lock — -r pylock.toml, also experimental — only landed a full year later in 26.1 (2026-04-26). That gap has a sharp edge: pip 26.0 and earlier don't detect the format by filename and parse it as a requirements file, producing Invalid requirement: 'lock-version = "1.0"', which tells the user nothing useful. 26.2 (2026-07-29) added --only-final support, the upload-time field, better conflict errors, and a security fix worth calling out: a package path in a pylock.toml fetched over a URL is now rejected if it resolves outside the lock file's own location, so a remote lock can no longer point at the local filesystem or another host. Current release is 26.2.1 (2026-08-04). Both the producing and consuming sides remain labelled experimental, and the docs state plainly that the generated lock "is only guaranteed to be valid for the current python version and platform."

uv has the widest surface: uv export --format pylock.toml, uv pip compile -o pylock.toml, uv pip sync pylock.toml, uv pip install -r pylock.toml, and uv run --with-requirements pylock.toml. It still prints The --pylock option is experimental and may change without warning as of 0.12.2 (2026-08-05). uv 0.12.0 (2026-07-28) tightened validation in ways that matter: a missing packages array is no longer treated as an empty lock (previously uv pip sync would happily uninstall your entire environment), filenames are enforced against the spec regex, and a declared artifact size must match what's downloaded. uv's position is unambiguous — its own docs say some of its functionality "cannot be expressed in the pylock.toml format; as such, uv will continue to use the uv.lock format within the project interface."

PDM is the only tool treating it as a native lock format rather than an export target. 2.24.0 (2025-04-18) added export; 2.25.0 (2025-06-13) added pdm config lock.format pylock, after which pdm lock writes pylock.toml instead of pdm.lock and reads it back. If cross-tool interop is a hard requirement for your team, PDM is currently the shortest path.

Poetry is the most conservative. Exporting requires Poetry 2.3.0 (2026-01-18) together with poetry-plugin-export 1.10.0, then poetry export -f pylock.toml --output pylock.toml. The release announcement states directly that "Poetry is not yet able to replace poetry.lock with pylock.toml."

pipenv ships a separate pipenv pylock --generate / --validate / --from-pyproject surface, but VCS, directory, and archive sources are still listed under future enhancements.

Don't write your own parser

The most under-advertised piece of this rollout is that packaging now ships a reference implementation. packaging.pylock landed in 26.0 (2026-01-20), Pylock.select() in 26.1 (2026-04-14), and 26.3 (2026-08-03) added a prefer_sdist_predicate argument for forcing sdists over wheels.

import tomllib
from packaging.pylock import Pylock

lock = Pylock.from_dict(tomllib.load(open("pylock.toml", "rb")))
for package, artifact in lock.select(dependency_groups=["dev"]):
    print(package.name, package.version, artifact.filename)

select() takes environment, tags, extras, dependency_groups, and the sdist predicate, and yields (Package, artifact) pairs — it implements the filtering algorithm the spec describes, including the convergence check. Duplicate a package entry and it raises PylockSelectError: Multiple packages with the name 'click' are selected at packages[8] and packages[0]. Read the documented validation scope before you rely on it: URL and path fields including packages.index are stored as-is and not validated, relative paths are not resolved against the lock file's location, hash algorithm names and digest formats aren't checked, and nothing is downloaded — verifying hashes and sizes against real artifacts is the caller's job.

How to actually use this

Don't make it your team's source of truth. If you use uv or Poetry, the truth in your repository stays uv.lock or poetry.lock (for how uv splits workspaces and verifies that lock, see uv Workspaces and Lockfiles). Swapping in pylock.toml costs you dependency-group boundaries (uv flattens the default group into unconditional entries — I confirmed --all-groups still emits no dependency-groups key), conflict declarations, and any ability to update one dependency without redoing everything. PDM users are the exception, because PDM genuinely round-trips it.

Do make it a build artifact. Export from your native lock in CI, ship it with the image, and use it for deployment installs, air-gapped installs, SBOM input, and audit trails. Mandatory hashes plus a flat structure plus no install-time resolver is precisely the right shape for that job, and it's the one job the format was designed for.

Split environments with named files. Since groups and platforms may be flattened, don't expect one file to cover everything. Emit pylock.prod.toml and pylock.ci.toml; the filename becomes the documentation. The spec allows named variants specifically so this works.

Validate portability on the consuming side. Before installing a pylock.toml you didn't produce, check three things: does it have requires-python, do any packages carry a marker, and does every package have exactly one wheel? All three pointing the wrong way means you're holding a snapshot of somebody's laptop. Treat it accordingly.

PEP 751 did the hard, useful thing: it pulled the resolution-output layer out of five proprietary formats and made it inspectable, hashable, and installable by anything. What it is not — despite how it gets described — is Python's Cargo.lock. It's a portable installation manifest, not a maintainable statement of dependency truth. Keep those two jobs separate and the current state of tool support is already good enough to build on.