How to Lock Python Dependencies With pip’s pylock.toml (PEP 751)
A hands-on tutorial that generates a PEP 751 pylock.toml file with pip's experimental lock command, explains its structure and hash pinning, and proves it works across tools by installing the same...
Every Python project eventually hits the same problem: pip install -r requirements.txt can install different package versions today than it did last month, even though the file itself never changed. A line like requests>=2.30 is a promise about a minimum version, not a guarantee of an exact one, so two developers running the same command on two different days can end up with two different environments. That gap between “what the file says” and “what actually got installed” is where a lock file comes in: a separate, machine-generated record of the exact versions (and exact files) that were installed, so the next install reproduces precisely the same environment.
Table Of Content
- What Problem Does a Lock File Actually Solve?
- Prerequisites
- Step 1: Check Your pip Version, and Upgrade It if Needed
- Step 2: Create a Scratch Project to Lock
- Step 3: Generate Your First pylock.toml With pip lock
- Step 4: Read and Understand the pylock.toml Structure
- Why the Hash Matters
- Step 5: Reproduce the Exact Environment From the Lock File
- Verify the versions match exactly
- Step 6: Prove pylock.toml Is Tool-Agnostic by Installing It With uv
- How pylock.toml Differs From requirements.txt
- Common Mistakes and Gotchas
- How to Confirm This All Worked End to End
- Next Steps
Python has lacked a standard, tool-agnostic lock file format for most of its history. Tools like Poetry, PDM, and uv each built their own, but a lock file from one tool was meaningless to another. PEP 751, written by Python packaging contributor Brett Cannon and accepted as a Final standard in March 2025, defines a shared format for this: pylock.toml. pip added an experimental implementation, and this tutorial builds one from scratch: generating a pylock.toml with pip, reading what is actually inside it, reproducing an environment from it, and proving the “tool-agnostic” part by installing that same file with a completely different tool, uv. Every command below was run against a real installation on Ubuntu 26.04 LTS with Python 3.11.15, pip 26.1.2, and uv 0.11.16 while writing this tutorial, and the file format itself was cross-checked against the official PEP 751 specification.
What Problem Does a Lock File Actually Solve?
A requirements.txt file mixes two different jobs into one document: it declares what your project depends on, and, if you pin exact versions, it can also serve as a rough record of what got installed. Loosely pinned requirements make upgrades easy but installs unpredictable. Tightly pinned requirements make installs predictable but say nothing about a package’s dependencies, so a sub-dependency can still drift. Neither variant records which specific file (wheel or source archive) was downloaded, or a hash to prove that file has not been tampered with.
The PEP 751 specification itself frames the motivation partly around security: it points to “concerns around the lack of secure defaults in the face of supply chain attacks” as one of the reasons a standard format was worth building, alongside the fact that at least five separate tools (PDM, plain pip freeze, pip-tools, Poetry, and uv) had already independently invented their own answers to the same problem. A pylock.toml file is designed to close that gap: it records every package, its exact version, the exact file that should be downloaded, and a cryptographic hash of that file, in a format any compliant tool can read without needing to re-run dependency resolution. That last part matters for speed and security alike: an installer reading a lock file does not need to contact a package index and work out which versions satisfy which constraints, it just installs exactly what the file says.
Prerequisites
- A Linux, macOS, or Windows machine with a terminal. This tutorial was verified on Ubuntu 26.04 LTS, but nothing here is Linux-specific.
- Python 3.11 or newer already installed, with access to
python3 -m venvfor creating virtual environments. - Comfort running basic shell commands (
mkdir,cd) and editing a text file. No prior experience with lock files, TOML, or Python packaging internals is assumed; every concept is explained the first time it comes up. - An internet connection, since pip and uv both need to download real packages from PyPI to demonstrate this.
- Optional but recommended for Step 6: the uv package manager installed, to prove the lock file works outside of pip. Everything through Step 5 only needs pip.
Step 1: Check Your pip Version, and Upgrade It if Needed
The pip lock command is brand new and experimental, so it is only available in recent pip releases. Check what you currently have:
python3 -m pip --version
If that reports something older than pip 25, or if you try the lock command later in this tutorial and see ERROR: unknown command "lock", upgrade pip first. That exact error is not hypothetical: testing this tutorial started on a machine with pip 24.0, which has no knowledge of pip lock at all. Upgrading resolved it immediately:
python3 -m pip install --upgrade pip
python3 -m pip --version
Expected output: something like pip 26.1.2 from /path/to/site-packages/pip (python 3.11). The exact version number will keep climbing after this tutorial is published, and that is fine; what matters is that it reports 25 or newer.
Step 2: Create a Scratch Project to Lock
Work in an isolated virtual environment so nothing here touches your system Python or any other project:
# Context: any working directory, Ubuntu 26.04 LTS or equivalent
mkdir pylock-tutorial && cd pylock-tutorial
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
Now write a small, deliberately loose requirements file, the kind almost every Python project starts with:
echo "requests==2.32.3" > requirements.txt
cat requirements.txt
This pins the top-level package, requests, but says nothing about its own dependencies (urllib3, certifi, idna, and charset-normalizer), which is exactly the gap a lock file exists to close.
Step 3: Generate Your First pylock.toml With pip lock
Run the new command against the requirements file:
pip lock -r requirements.txt -o pylock.toml
Expected output starts with a warning, then a normal-looking dependency resolution:
WARNING: pip lock is currently an experimental command. It may be removed/changed in a future release without prior warning.
Collecting requests==2.32.3 (from -r requirements.txt (line 1))
Collecting charset-normalizer<4,>=2 (from requests==2.32.3->-r requirements.txt (line 1))
Collecting idna<4,>=2.5 (from requests==2.32.3->-r requirements.txt (line 1))
Collecting urllib3<3,>=1.21.1 (from requests==2.32.3->-r requirements.txt (line 1))
Collecting certifi>=2017.4.17 (from requests==2.32.3->-r requirements.txt (line 1))
Notice pip resolved the full dependency tree, not just the one package you listed. That resolution work happens once, at lock time; anyone installing from the resulting file later skips it entirely. Confirm the file exists before moving on:
ls -la pylock.toml
If instead you saw ERROR: unknown command "lock", go back to Step 1 and upgrade pip; this command genuinely does not exist before pip added experimental support for it.
Step 4: Read and Understand the pylock.toml Structure
Open the file pip just generated. It is plain TOML, readable without any special tooling:
cat pylock.toml
The top of the file has two lines that apply to the whole document:
lock-version = "1.0"
created-by = "pip"
lock-version identifies which version of the PEP 751 format this file follows, so a future installer can tell whether it understands the file before trying to use it. created-by records which tool generated it; this field is part of why the format is tool-agnostic, an installer does not need to know or care that pip specifically wrote this file to be able to consume it, but the provenance is preserved anyway.
Below that, every dependency gets its own [[packages]] block. Here is the entry for certifi, straight from a real generated file (the download URL is shortened below for readability; the real one is a single long line):
[[packages]]
name = "certifi"
version = "2026.7.22"
[[packages.wheels]]
name = "certifi-2026.7.22-py3-none-any.whl"
url = "https://files.pythonhosted.org/packages/0b/a7/.../certifi-2026.7.22-py3-none-any.whl"
[packages.wheels.hashes]
sha256 = "62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775"
Why the Hash Matters
The sha256 value under packages.wheels.hashes is a cryptographic fingerprint of that exact wheel file. When you later install from this lock file, pip downloads the wheel from the recorded URL, hashes it, and refuses to install it if the hash does not match. That is a real defense against a specific, documented category of supply chain attack: a compromised or malicious index serving a different file under the same package name and version than the one that was actually reviewed and locked. A loosely pinned requirements.txt offers no such check; a fully pinned one with exact versions still does not, unless you separately maintain a hashes file yourself. In pylock.toml, the hash is not optional extra work, it is generated automatically every time.
Step 5: Reproduce the Exact Environment From the Lock File
The real test of a lock file is whether a second, completely fresh environment installs identically from it. Create one:
cd ..
python3 -m venv .venv-reproduce
.venv-reproduce/bin/pip install --upgrade pip
.venv-reproduce/bin/pip install -r pylock-tutorial/pylock.toml
Expected output again opens with an experimental-feature warning, this time for reading rather than generating:
WARNING: Using pylock.toml as a requirements source is an experimental feature. It may be removed/changed in a future release without prior warning.
Collecting certifi==2026.7.22 (from pylock.toml)
Collecting charset-normalizer==3.4.9 (from pylock.toml)
Collecting idna==3.18 (from pylock.toml)
Collecting requests==2.32.3 (from pylock.toml)
Collecting urllib3==2.7.0 (from pylock.toml)
Successfully installed certifi-2026.7.22 charset-normalizer-3.4.9 idna-3.18 requests-2.32.3 urllib3-2.7.0
Verify the versions match exactly
.venv-reproduce/bin/pip freeze
Compare that output against the versions recorded in pylock.toml from Step 4. Every version should match precisely, not “close enough”: that exact match, reproduced in a brand new environment with no other input than the lock file, is the entire point.
Step 6: Prove pylock.toml Is Tool-Agnostic by Installing It With uv
This is the step that actually tests the “tool-agnostic” claim in PEP 751’s own name: a file pip generated, read by a completely different tool with its own independent implementation. If you have uv installed, try it:
cd pylock-tutorial
uv venv .venv-uv
uv pip sync pylock.toml --python .venv-uv/bin/python3
Expected output:
warning: The `--pylock` option is experimental and may change without warning. Pass `--preview-features pylock` to disable this warning.
Prepared 5 packages in 93ms
Installed 5 packages in 10ms
+ certifi==2026.7.22
+ charset-normalizer==3.4.9
+ idna==3.18
+ requests==2.32.3
+ urllib3==2.7.0
uv correctly recognized and parsed the file as a pylock.toml, without being told which tool generated it, and installed the exact same five packages at the exact same versions. Neither tool needed to know anything about the other; the file format is the entire contract between them. That is a meaningfully different guarantee than requirements.txt ever offered, where subtle differences in how different tools parsed edge cases could produce different results from the “same” file.
How pylock.toml Differs From requirements.txt
- Structure.
requirements.txtis loosely defined line-oriented text that grew organically and that different tools parse with slightly different edge-case behavior.pylock.tomlis standard TOML with a specification that defines every field. - Hashes by default.
pip lockrecords a hash for every package automatically. Hash-checking withrequirements.txtexists, but it is opt-in and easy to skip. - No resolution needed at install time. A
pylock.tomlfile already contains the fully resolved dependency tree. An installer reading it does not need to contact a package index and work out which versions satisfy which constraints; it can go straight to downloading and verifying the exact files listed. - Scoped validity. This is a real limitation, stated directly in pip’s own
pip lock --helpoutput: “The generated lock file is only guaranteed to be valid for the current python version and platform.” A lock file generated on Linux with Python 3.11 is not automatically safe to reuse on Windows or with Python 3.9; PEP 751 does define optional fields for multi-environment locking, but a plainpip lockrun like the one in this tutorial only captures the environment it ran on.
Common Mistakes and Gotchas
- Assuming any recent pip supports this. The plain “unknown command” error from Step 1 is exactly what you get on pip 24.0 and earlier. There is no flag or workaround; the command simply does not exist yet in older releases.
- Upgrading pip just enough to unlock
pip lock, but not enough to read the result. According to pip’s own changelog, experimental support for generating a lock file and experimental support for installing from one (pip install -r pylock.toml) were added in separate releases, with read support landing later. Ifpip lockworks butpip install -r pylock.tomlfails with a format error, upgrade pip again rather than assuming the file itself is broken. - Treating this as stable. Both warnings you saw in this tutorial say plainly that the feature “may be removed/changed in a future release without prior warning.” Pin your pip and uv versions in CI if you depend on this workflow, so an upstream change does not silently break a pipeline.
- Forgetting uv needs an explicit target environment.
uv pip syncdoes not always guess which environment you mean; passing--pythonpointing at the interpreter inside your target virtual environment, as shown in Step 6, avoids installing into the wrong place or failing outright. - Confusing this with a tool’s native lock file. If your project already uses Poetry’s
poetry.lock, PDM’spdm.lock, or uv’s ownuv.lock, those remain that tool’s primary workflow.pylock.tomlis an interchange format, most useful for exporting a lock file so a different tool, or a plainpip install, can consume it, not necessarily a wholesale replacement for a tool’s native format.
How to Confirm This All Worked End to End
- Regenerate the lock file from the same requirements with
pip lock -r requirements.txt -o pylock-check.tomland diff it against the original withdiff pylock.toml pylock-check.toml. Identical output confirms the lock is deterministic for a given input and environment, not random each run. - Run
pip freezeinside the fresh environment from Step 5 and confirm every version matches theversionfield for that package insidepylock.toml, not just “close” versions. - Run the uv sync from Step 6 a second time and confirm it reports the packages are already installed and does no extra work, proving the two tools agree on what “correct” looks like.
- Deliberately edit one version number inside
pylock.tomlto something that does not exist on PyPI, then trypip install -r pylock.tomlagain and confirm it fails loudly instead of silently installing something else. A lock file that fails safe on tampering is doing its job.
Next Steps
Everything above locked a plain requirements.txt, but pip lock also accepts a local project directory containing a pyproject.toml directly, for example pip lock . run from inside a project whose pyproject.toml declares its dependencies in the standard PEP 621 [project] table, which resolves and locks that project’s declared dependencies the same way. Since this whole workflow is still explicitly experimental, keep an eye on the pip changelog as it evolves toward stabilization. If the hash-verification angle in Step 4 was interesting, the natural next stop is supply chain integrity for the artifacts you ship, not just the ones you install; this tutorial on generating and signing an SBOM with Syft and Cosign covers the same trust-but-verify idea one layer up, at the container image level instead of the individual package level.








No Comment! Be the first one.