Keep Dev Tools Out of Production with Poetry Dependency Groups
Use Poetry dependency groups to keep pytest, black, and mypy out of production while one committed lock file keeps every environment reproducible. Covers group syntax, install flags, and verification.
07 Apr 2026, 18:00 UTC

Your production container needs your web framework and database driver. It does not need pytest, black, mypy, or the half-dozen linters your team runs locally. If you install everything everywhere, production images grow, attack surface grows, and installs slow down. Poetry's dependency groups solve this: declare tooling in a separate group, keep one lock file for reproducibility, and install only what each environment needs.
What you need before starting
Dependency groups require Poetry 1.2 or newer. Check your version first, because group-related CLI flags changed across 1.x releases:
poetry --versionRun this in any terminal where Poetry is on your PATH. If you are on 1.1.x, upgrade before following along; the [tool.poetry.group] section will not be recognized. You also need an existing project with a pyproject.toml created via poetry init or poetry new.
Declaring runtime and dev dependencies separately
Runtime dependencies belong in the main section. Development tools go in a named group. A typical pyproject.toml looks like this:
[tool.poetry.dependencies]
python = "^3.11"
fastapi = "^0.110"
sqlalchemy = "^2.0"
[tool.poetry.group.dev.dependencies]
pytest = "^8.0"
black = "^24.0"
mypy = "^1.8"Add packages to a group from the command line instead of editing by hand:
poetry add fastapi sqlalchemy
poetry add --group dev pytest black mypyRun these from the project root. The --group dev flag places the requirement under the dev group rather than the main dependencies. Poetry resolves versions and writes them into poetry.lock automatically.
Controlling what gets installed
By default, poetry install installs the main dependencies plus every non-optional group. That is what you want on a developer laptop. For a production build, exclude the dev group:
poetry install --without devFor the tightest possible deployment environment, install only the main group:
poetry install --only mainThe difference matters: --without dev still installs any other non-optional groups you may have defined (docs, lint, test), while --only main installs strictly the runtime set. In a Dockerfile, prefer --only main so future groups do not silently leak into the image.
If a group is only needed in specific pipelines, mark it optional so it is skipped by default:
[tool.poetry.group.docs]
optional = true
[tool.poetry.group.docs.dependencies]
mkdocs = "^1.5"Then request it explicitly where needed: poetry install --with docs.
Why the lock file is the reproducibility piece
Groups control which packages install; poetry.lock controls exactly which versions. The lock file records pinned versions and content hashes for the entire resolved tree, including transitive dependencies. Commit it to version control. Anyone who clones the project and runs poetry install gets the identical tree, whether they are on a laptop, in CI, or building a container.
Two commands people routinely confuse:
poetry lock— re-resolves dependencies after you change version constraints inpyproject.toml, refreshing the lock file without necessarily upgrading anything.poetry update— re-resolves and upgrades packages to the newest versions allowed by your constraints.
Never edit poetry.lock by hand. If you change pyproject.toml and forget to re-lock, poetry install fails with an error telling you the lock file is out of sync. The fix is poetry lock, then commit both files together.
Verifying the split actually worked
Do not trust the flags blindly; verify in a clean environment. Poetry creates an isolated virtualenv per project by default, so you can inspect exactly what landed:
poetry env remove --all
poetry install --only main
poetry run pip listRun these from the project root. poetry env remove --all deletes the project's virtualenvs so you start fresh (this is destructive to that environment but not to your code). In the package list, confirm pytest, black, and mypy are absent while fastapi and sqlalchemy are present. Then confirm the application still works:
poetry run python -c "import your_package"Replace your_package with your actual importable package name. For consistency checks, run:
poetry check
poetry lock --checkpoetry check validates pyproject.toml; poetry lock --check confirms the lock file matches it (available in recent 1.x versions — if the flag is unknown on your version, a plain poetry install will surface the same mismatch as an error).
The strongest verification is a clean-room install: clone the repository into a fresh directory or container, run poetry install, start the application, and run the test suite. If that works, the lock file genuinely reproduces your environment.
Limitations and recovery
A few constraints to plan around:
- Groups are a Poetry 1.2+ feature. Older tutorials referencing
dev-dependenciesdescribe the legacy section, which behaves like a dev group but is deprecated in newer releases. - Poetry manages its own virtualenvs. If you need to install into a pre-existing environment (common in slim Docker images), use
poetry config virtualenvs.create falsein that build context, understanding that packages then land in the active interpreter. - If an install goes wrong — a bad resolve, a corrupted environment — recovery is straightforward:
poetry env remove --allfollowed bypoetry installrebuilds from the lock file. To undo a bad dependency change, revertpyproject.tomlandpoetry.lockin version control and reinstall; this is why committing the lock file matters.
The end state to aim for: one pyproject.toml declaring intent, one committed poetry.lock pinning reality, and per-environment install flags that give developers their tools and production only what it runs.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.