Poetry's Lock File: Making Dependency Resolution Predictable and Reproducible
pyproject.toml declares what you want; poetry.lock records what you actually get. A look at Poetry's resolver, a verifiable example, and the trade-offs.
24 Jan 2026, 15:32 UTC

Same pyproject.toml, different packages: the problem a lock file solves
On Monday, a teammate runs poetry install and gets requests 2.31.0. On Friday, CI pulls requests 2.32.1 because someone re-resolved dependencies and forgot to commit the result. Same manifest, different software—and a bug that only exists on one of the two machines. Poetry closes this gap with two files playing distinct roles: pyproject.toml declares what you want (version ranges, extras, Python constraints), while poetry.lock records the exact packages the resolver picked, including transitive dependencies and checksums, so every install reproduces the same result.
The rest of this post covers how that solve works, a small example you can verify yourself, and the trade-offs worth knowing before you commit the lock file.
From version ranges to a solved graph
When you run poetry lock or poetry add, Poetry treats your constraints as a constraint-satisfaction problem: it gathers package metadata from the configured source, walks transitive dependencies, and searches for one set of versions satisfying every range at once—your direct constraints, your dependencies' constraints, and the Python version requirement. If package A needs urllib3 < 3 and package B needs urllib3 >= 2, the solver finds a version fitting both or reports a conflict rather than silently picking one.
Three details matter in practice:
- Markers are part of the solve. Dependencies can carry environment markers such as
sys_platform == 'win32', so one lock file can serve Windows, macOS, and Linux; the appropriate wheel is chosen at install time. - Extras are recorded, not installed. Optional groups land in the lock file but are only installed when you pass
--extras. - Entries carry hashes. Each package entry stores SHA-256 checksums for its artifacts, so installs verify integrity instead of trusting whatever a mirror serves.
A worked example, with checks you can run
Here is a small pyproject.toml with a ranged dependency, a Windows-only package, and an optional extra (illustrative—the version numbers are examples, not recommendations):
[tool.poetry]
name = "report-generator"
version = "0.1.0"
description = "Generates PDF reports from API data"
authors = ["Your Name <[contact removed]>"]
[tool.poetry.dependencies]
python = "^3.10"
requests = "^2.31"
colorama = { version = "^0.4.6", markers = "sys_platform == 'win32'" }
reportlab = { version = "^4.0", optional = true }
[tool.poetry.extras]
pdf = ["reportlab"]
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
Run these from the project root (the directory containing pyproject.toml). No special permissions are needed beyond write access to the project and network access to your package source; Poetry creates and uses its own virtual environment, so no manual activation is required:
poetry lock # solve constraints, write poetry.lock
poetry install # install exactly the locked set
poetry install --extras pdf # include the optional group
poetry show --tree # print the resolved dependency graph
poetry check # validate pyproject/lock consistency
What to expect:
poetry.lockgains a[[package]]block forrequestswith one exact version and a list of sha256 file hashes. Don't hand-edit these; regenerate instead.poetry show --treelists requests with its transitive dependencies (certifi, charset-normalizer, idna, urllib3) at the locked versions—your check that the installed tree matches the lock.poetry checkpasses when the manifest and lock agree; it complains if you edited the lock by hand or changedpyproject.tomlwithout re-locking.
To confirm the active version at runtime: poetry run python -c "from importlib.metadata import version; print(version('requests'))"—it should match the lock entry exactly.
Trade-offs worth knowing first
- Lock behavior differs across major versions. In Poetry 1.x, plain
poetry lockre-resolved everything and--no-updatepreserved pins; Poetry 2.x flipped the default—lockkeeps pins and--regenerateforces a full re-solve. Runpoetry lock --helpon your installed version before scripting it, and pin the Poetry version in CI images so everyone solves with the same resolver. - Large graphs solve slowly. The solver explores the combined constraint space, so deep dependency trees can make locking much slower than installing.
- Extras are opt-in everywhere. Missing
--extrasin one environment surfaces as aModuleNotFoundErrorat runtime, not at install. Document required extras in your README and CI config. - A lock is not a wheel pin. It records versions and checksums but can't force a specific build variant, such as a CPU-optimized wheel, without overrides or a custom package source.
- Lock files are large. Thousands of lines of hashes are normal. Commit it anyway; the reproducibility outweighs the repository bytes.
If a lock update goes wrong
Both commands change state—poetry lock rewrites the lock file, poetry install changes the environment. Since the lock lives in version control, rollback is ordinary git: revert poetry.lock to the previous commit, then re-sync (poetry install --sync on Poetry 1.x, poetry sync in 2.x) so installed packages match the restored file.
Make the lock file earn its keep
Commit poetry.lock, install with poetry install rather than ad-hoc pip commands, and run poetry check plus poetry install in CI so a drifted lock fails loudly instead of quietly producing a different build. When you upgrade deliberately, do it as its own commit—poetry update requests—so the diff shows exactly which pins moved and reverting is a single git revert away. The flag specifics above reflect Poetry 1.8–2.x behavior; confirm against the docs for your installed version.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.