Lock Files & Reproducible Installs
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the Python Reference bibliography. |
A lock file records the exact set of distributions that an environment was, or should be, built from, so that the next install
gives the same result. This page compares the lock formats that pip and Poetry read and write. It was written against pip 26.2.2,
Poetry 2.5.1 with poetry-plugin-export 1.10.1, and the examples were run with CPython 3.14.6 on 2026-10-22 against PyPI on that
day, so the versions shown are the ones current then. Long sha256 values inside quoted TOML strings are shortened with ….
Abstract and concrete requirements
The Packaging User Guide distinguishes two kinds of dependency lists:
-
Abstract requirements say what a project needs to work:
rich>=13. They belong in[project] dependencies(historically setuptools'install_requires). They should be as loose as possible, because a library is installed next to other packages with their own requirements, and the resolver must satisfy all of them at once. -
Concrete requirements say exactly what to install:
rich==15.0.0, from this index, with this hash. They describe one environment rather than one project, and they belong in a lock file.
The birdtally library therefore declares rich>=13 for its pretty extra, while the birdtally-report application that uses
it locks rich==15.0.0 and everything below it.
The options
pip freeze
The oldest "lock file" is the output of pip freeze from a working environment:
python -m pip install -e ".[pretty]"
python -m pip freeze
# Editable install with no version control (birdtally==0.1.0)
-e …/birdtally
markdown-it-py==4.2.0
mdurl==0.1.2
Pygments==2.21.0
rich==15.0.0
It pins every installed distribution, but nothing else: no hashes, no index, no record of which packages were requested and
which were pulled in, and only the platform it ran on. A dependency that is installed only on Windows (colorama; sys_platform == "win32")
is simply absent from a freeze made on Linux. It also freezes whatever happens to be installed, including
tools you added by hand. Use --exclude-editable to leave out the project itself, and -r requirements.in to keep your own ordering
and comments. See pip: Managing Installed Packages.
Source: pip: Pinning the package versions.
Hashed requirements files
Adding --hash options to every line turns a requirements file into a verifiable lock: pip then refuses any file whose hash is not
listed (hash-checking mode). The export from Poetry is one way to produce such a file:
poetry export -f requirements.txt --only main -E pretty
markdown-it-py==4.2.0 ; python_version >= "3.10" \
--hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \
--hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a
mdurl==0.1.2 ; python_version >= "3.10" \
--hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \
--hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba
pygments==2.21.0 ; python_version >= "3.10" \
--hash=sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9 \
--hash=sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c
rich==15.0.0 ; python_version >= "3.10" \
--hash=sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb \
--hash=sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36
Each line lists the hash of every file of that version that may be installed (here the wheel and the sdist). The third-party
pip-compile from pip-tools produces the same kind of file from a requirements.in, and pip hash computes a single hash. How
hash-checking mode behaves, including the error for a tampered file, is on
pip: Repeatable & Secure Installs.
Source: pip: Hash-checking.
Constraints files
A constraints file (-c constraints.txt, PIP_CONSTRAINT) has the same syntax as a requirements file but only limits versions:
a package listed in it is installed only if something else requires it. It is the tool for "these versions everywhere" across many
projects or a monorepo, not a lock for one project. poetry export -f constraints.txt writes one from poetry.lock. Since pip 26.2
constraints no longer apply to isolated build environments; build dependencies have their own --build-constraint. See
pip: Constraints & Build Constraints.
pylock.toml (PEP 751)
PEP 751 (Final, 2025) standardised a lock file that any installer can read: TOML, named pylock.toml or pylock.<name>.toml
(the name must match ^pylock\.([^.]+)\.toml$, so pylock.dev.toml is valid and pylock.dev.linux.toml is not). Its top-level
keys are lock-version (currently "1.0"), environments (the markers it is valid for), requires-python, extras,
dependency-groups, default-groups, created-by and the [[packages]] array. Each package records its name, version, the
index it came from, and the exact files with their url, size, upload-time and hashes; a package can also be a
directory, a vcs checkout or an archive. Packages may carry a marker that refers to the extras and dependency_groups
marker variables, so one file can serve several group combinations. An installer must not resolve anything itself: it evaluates
the markers and installs exactly what is listed.
Two tools write it in this section:
-
pip lock(pip 25.1, experimental) resolves the requirements you give it and writes apylock.tomlfor the current platform and Python only. -
poetry export -f pylock.toml(poetry-plugin-export 1.10.0 and later) convertspoetry.lock, so it is as cross-platform as the Poetry lock it came from.
The Poetry export for the birdtally pretty extra (only the first package shown; the other three have the same shape):
poetry export -f pylock.toml --only main -E pretty -o pylock.toml
lock-version = "1.0"
environments = ["python_version >= \"3.10\""]
requires-python = ">=3.10"
created-by = "poetry-plugin-export"
[[packages]]
name = "markdown-it-py"
version = "4.2.0"
requires-python = ">=3.10"
index = "https://pypi.org/simple"
sdist = {name = "markdown_it_py-4.2.0.tar.gz", url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", upload-time = 2026-05-07T12:08:28.360612Z, size = 82454, hashes = {sha256 = "04a21681…e418d49"}}
wheels = [
{name = "markdown_it_py-4.2.0-py3-none-any.whl", url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", upload-time = 2026-05-07T12:08:27.182789Z, size = 91687, hashes = {sha256 = "9f7ebbcd…2641c4a"}},
]
…
[tool.poetry-plugin-export]
groups = ["main"]
extras = ["pretty"]
The plugin refuses any other file name:
The output file for pylock.toml export must be named "pylock.toml" or must follow the regex "^pylock\.([^.]+)\.toml$", e.g. "pylock.dev.toml"
Since pip 26.1, pip install -r pylock.toml installs such a file, also as an experimental feature:
python -m pip 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.
Collecting markdown-it-py==4.2.0 (from pylock.toml)
Using cached markdown_it_py-4.2.0-py3-none-any.whl (91 kB)
Collecting mdurl==0.1.2 (from pylock.toml)
Using cached mdurl-0.1.2-py3-none-any.whl (10.0 kB)
Collecting pygments==2.21.0 (from pylock.toml)
Using cached pygments-2.21.0-py3-none-any.whl (1.3 MB)
Collecting rich==15.0.0 (from pylock.toml)
Using cached rich-15.0.0-py3-none-any.whl (310 kB)
Installing collected packages: pygments, mdurl, markdown-it-py, rich
Successfully installed markdown-it-py-4.2.0 mdurl-0.1.2 pygments-2.21.0 rich-15.0.0
pip lock, the -r pylock.toml options and what pip 26.2 added (--only-final honoured, upload-time used by
--uploaded-prior-to, path packages in remote lock files rejected) are on
pip lock & pylock.toml.
poetry.lock
Poetry’s own lock file predates PEP 751 and is still what Poetry reads. It is written by poetry lock (and by add, remove and
update) and lists every package the project could need on any platform and for any group, with the groups and markers under
which each is needed:
# This file is automatically @generated by Poetry 2.5.1 and should not be changed by hand.
[[package]]
name = "click"
version = "8.5.0"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.10"
groups = ["docs"]
files = [
{file = "click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360"},
{file = "click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34"},
]
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
groups = ["dev", "docs", "test"]
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
markers = {dev = "sys_platform == \"win32\"", docs = "platform_system == \"Windows\"", test = "sys_platform == \"win32\""}
…
[metadata]
lock-version = "2.1"
python-versions = ">=3.10"
content-hash = "e6de9ee1…c8d825f4"
-
lock-version = "2.1"is the format version written by Poetry 2.x (2.1 added the per-packagegroupsandmarkerskeys). -
content-hashis a hash of the relevant parts ofpyproject.toml. When they change,poetry installrefuses to use the stale lock withpyproject.toml changed significantly since poetry.lock was last generated. Run `poetry lockto fix the lock file.` -
Unlike
pip freeze, the lock containscoloramaalthough the examples ran on Linux: it records the Windows-only dependency of pytest and click with its markers, so the same lock installs correctly on every platform.
The format and the install/update/sync workflow are on
Poetry: Lock File, install, update & sync.
Source: Poetry: Installing with poetry.lock.
What each one guarantees
| Format | Cross-platform | Hashes | Standard | Groups/extras | Read by |
|---|---|---|---|---|---|
|
No |
No |
No (pip format) |
No |
pip, uv, every tool that reads requirements files |
Hashed |
Only if written that way (export, pip-compile with markers) |
Yes |
No (pip format) |
One file per combination |
pip, uv |
Constraints file |
n/a (limits only) |
Optional |
No (pip format) |
n/a |
pip, uv |
|
No (one platform) |
Yes |
PEP 751 |
Yes (markers) |
pip 26.1+ (experimental), uv, PDM |
|
Yes, as far as |
Yes |
PEP 751 |
Selected at export |
as above |
|
Yes |
Yes |
No (Poetry format) |
Yes |
Poetry, poetry-plugin-export |
The Packaging User Guide’s tool recommendations still name only pip-tools and Pipenv for lock files at the time of writing; PEP 751 is the format the ecosystem is converging on, and uv, PDM and Poetry’s export plugin already write it.
Which lock file?
Commit it or not?
-
Applications (services, notebooks, birdtally-report): commit the lock file. Without it, two deploys a week apart can run different code. Poetry’s documentation says the same: commit
poetry.lockso that everyone works with the same versions. -
Libraries (birdtally): the lock file has no effect on your users, because their installer reads only your metadata and resolves against their other requirements. Poetry’s documentation leaves the choice to you: committing it makes CI reproducible, but your tests then lag behind what users actually get. If you commit it, refresh it regularly (for example a scheduled
poetry updatejob), and also test once against the newest versions.
Common mistakes
-
Freezing a development environment into production requirements. The freeze includes pytest, ruff and whatever else is installed. Lock from the declared requirements instead.
-
Running
pip install -e .after installing a lock. pip resolves the project’s own dependencies again and may upgrade what the lock pinned. Install the project with--no-depsafter the locked dependencies, or let Poetry’sinstallhandle both. -
Editing
poetry.lockby hand or resolving merge conflicts in it textually. Resolve thepyproject.tomlconflict and runpoetry lockagain. -
Assuming a
pip lockfile is portable. It is resolved for the platform it ran on. -
Expecting a library’s lock file to protect its users. Only metadata travels with the package.