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.

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.

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 a pylock.toml for the current platform and Python only.

  • poetry export -f pylock.toml (poetry-plugin-export 1.10.0 and later) converts poetry.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-package groups and markers keys).

  • content-hash is a hash of the relevant parts of pyproject.toml. When they change, poetry install refuses to use the stale lock with pyproject.toml changed significantly since poetry.lock was last generated. Run `poetry lock to fix the lock file.`

  • Unlike pip freeze, the lock contains colorama although 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.

What each one guarantees

Format Cross-platform Hashes Standard Groups/extras Read by

pip freeze output

No

No

No (pip format)

No

pip, uv, every tool that reads requirements files

Hashed requirements.txt

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

pylock.toml from pip lock

No (one platform)

Yes

PEP 751

Yes (markers)

pip 26.1+ (experimental), uv, PDM

pylock.toml from poetry export

Yes, as far as poetry.lock

Yes

PEP 751

Selected at export

as above

poetry.lock

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?

flowchart TD A[What are you shipping?] --> B{An application, service or container} A --> C{A library published to an index} B --> D{Which tool manages the project?} D -->|Poetry| E[Commit poetry.lock] D -->|pip| F{Need the same lock on several platforms?} F -->|No| G[pip lock to pylock.toml, experimental, or a hashed requirements.txt] F -->|Yes| H[A cross-platform locker: Poetry, uv or pip-compile per platform] E --> I{Deploy with plain pip?} I -->|Yes| J[poetry export to pylock.toml or requirements.txt] I -->|No| K[poetry install or poetry sync in the image] C --> L[No lock in the published metadata: keep dependencies loose] L --> M[Optionally commit a lock for CI only and refresh it often]

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.lock so 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 update job), 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-deps after the locked dependencies, or let Poetry’s install handle both.

  • Editing poetry.lock by hand or resolving merge conflicts in it textually. Resolve the pyproject.toml conflict and run poetry lock again.

  • Assuming a pip lock file 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.