Stop Shipping Your Test Suite: Poetry Lock Files and Dependency Groups for Lean, Reproducible Builds
Poetry's lock file pins your entire dependency graph while dependency groups keep pytest and friends out of production. Here's how to use both, and verify they actually work.
08 Aug 2025, 05:42 UTC

The problem: "works on my machine" and bloated production images
Two complaints show up constantly on Python teams. First, a deployment breaks even though the code didn't change, because pip install -r requirements.txt re-resolved a transitive dependency to a newer version. Second, the production container is full of pytest, black, mypy, and their entire dependency trees — packages that should never exist outside a developer laptop.
Poetry addresses both with two features that work together: the lock file, which pins the entire resolved graph, and dependency groups, which separate runtime requirements from development tooling. Used properly, you get one command that reproduces an exact environment anywhere, and a one-flag switch that strips dev packages out of a production install.
What the lock file actually guarantees
Your pyproject.toml declares ranges, like requests = "^2.31". Ranges are necessary — you want compatible updates — but they mean two installs a week apart can produce different environments. When you run poetry add requests or poetry lock, Poetry resolves the whole graph once, picks concrete versions that satisfy every constraint simultaneously, and writes them to poetry.lock along with content hashes of the artifacts.
After that, poetry install doesn't re-resolve. It reads the lock file and installs exactly those versions. A teammate cloning the repo, a CI runner, and your production build all get the same environment. If constraints in pyproject.toml conflict, the resolver fails loudly at lock time instead of leaving you with a broken mix that only explodes at runtime.
Two practices make this real:
- Commit
poetry.lockto version control. This is non-negotiable for applications. Libraries sometimes skip it so CI tests against fresh resolutions, but even there, committing it makes CI reproducible. - Add dependencies with
poetry add, not by hand-editing. One command updatespyproject.toml, re-resolves, and rewrites the lock file atomically. Hand-editing the manifest without re-locking is how drift creeps back in.
Dependency groups: keeping dev tooling out of production
Dependency groups let you declare packages that aren't part of the runtime. The modern syntax (Poetry 1.2+) looks like this in pyproject.toml:
[tool.poetry.dependencies]
python = "^3.11"
requests = "^2.31"
[tool.poetry.group.dev.dependencies]
pytest = "^8.0"
ruff = "^0.4"
mypy = "^1.10"By default, poetry install installs all groups — which is what a developer wants. For a production build, you exclude them:
# Run in your Dockerfile or deploy script; needs Poetry installed there
poetry install --only main --no-root--only main installs just the runtime dependencies; --no-root skips installing the project itself, which is handy in multi-stage Docker builds where you copy source in a later layer. (--without dev is the equivalent exclusion form.) Run poetry --version first and check the changelog for your major version — group syntax and installer flags changed across releases, and the older [tool.poetry.dev-dependencies] section you'll see in older tutorials is deprecated.
A worked example: verifying the split
In a scratch project (any directory, your normal user account, Poetry installed):
poetry new demoapp && cd demoapp
poetry add requests
poetry add --group dev pytest
poetry install --only main
poetry run python -c "import requests; print('runtime ok')"
poetry run python -c "import pytest" # expect ModuleNotFoundErrorThe last command should fail — that's the confirmation that dev tooling stayed out of this environment. Then verify reproducibility: delete the virtualenv (poetry env remove python or remove the path from poetry env info) and run plain poetry install. The resolved versions should match poetry.lock exactly; poetry check will also flag a lock file that's out of sync with pyproject.toml.
If your deployment target only speaks pip, the poetry export plugin (availability varies by version — check before relying on it) can emit a pinned requirements.txt from the lock, so the reproducibility survives the handoff.
Trade-offs worth knowing
- Resolver speed. On large graphs with wide-open ranges, locking can be slow. Tightening constraints usually helps more than faster hardware.
- The lock file is not a security boundary. Hashes verify you're getting the same artifacts you locked — not that those versions are free of known vulnerabilities. You still need an auditing step (e.g.,
pip-auditagainst an exported requirements file). - Version churn. Commands and defaults differ between Poetry major versions. Pin the Poetry version in CI the same way you pin everything else.
Where to start
Pick one application repo. Commit its poetry.lock, move test and lint tools into a dev group, and change the production install to poetry install --only main. Then do the one check that proves the whole setup: blow away the virtualenv, reinstall from the lock, and confirm the environment comes back identical — and noticeably smaller.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.