Stop Installing pytest in Production: Poetry Dependency Groups Explained
Poetry's dependency groups (1.2.0+) let you split runtime, test, lint, and optional dependencies into named sets and install only what each CI stage or environment needs — smaller images, faster pipelines.
07 Jan 2026, 08:36 UTC

The problem: one flat pile of dependencies
Most Python projects start with a simple split: runtime dependencies in one list, development tools in another. That works until your CI pipeline has four stages — lint, test, build, deploy — and every stage installs everything. Your deploy image carries pytest, black, mypy, and whatever else accumulated, and your lint job waits while pip resolves a hundred packages it will never import.
Poetry's dependency groups, introduced in version 1.2.0, solve this cleanly. Instead of the old two-bucket model ([tool.poetry.dependencies] plus [tool.poetry.dev-dependencies]), you define as many named groups as you need and install exactly the ones each environment requires. The takeaway: faster CI, smaller production environments, and a pyproject.toml that documents intent instead of hiding it.
How groups work
Each group lives under [tool.poetry.group.<name>.dependencies]. Your runtime dependencies stay in [tool.poetry.dependencies], which Poetry treats as an implicit group named main. The legacy dev-dependencies section still works for backward compatibility — Poetry internally maps it to a group called dev — but new projects should use explicit groups.
A typical layout looks like this:
[tool.poetry.dependencies]
python = "^3.11"
fastapi = "^0.110"
[tool.poetry.group.test.dependencies]
pytest = "^8.0"
httpx = "^0.27"
[tool.poetry.group.lint.dependencies]
ruff = "^0.4"
mypy = "^1.9"
[tool.poetry.group.docs]
optional = true
[tool.poetry.group.docs.dependencies]
mkdocs-material = "^9.5"Two things to notice. First, groups are just named sections — no plugins or extra tooling. Second, the docs group sets optional = true, which makes it opt-in: it is excluded from default installs entirely and only installed when you name it explicitly.
Selective installation with --only, --without, and --with
Three flags control which groups get installed. Run these from the project root; they modify the project's virtualenv, so no special permissions are needed beyond write access to the project and Poetry's environment:
poetry install --only main— installs exactly the runtime group and nothing else. Ideal for production images.poetry install --with test— installs the default set (main plus non-optional dev groups) plus the named group.poetry install --without lint— installs the default set minus the named group.
In CI this maps directly onto stages. A lint job runs poetry install --only lint; a test job runs poetry install --only main,test. Each stage resolves and downloads only what it needs, which is where the pipeline time savings come from.
The same flags apply to poetry export, which matters when you deploy to systems that consume a plain requirements.txt:
poetry export --only main -f requirements.txt --output reqs.txtTo verify the result, compare the output against your expectations: grep pytest reqs.txt should return nothing, and poetry show --tree --only main should show a runtime-only dependency graph. The lock file (poetry.lock) records group membership for each package, so selective installs stay reproducible across machines without re-resolving.
Trade-offs and gotchas
Groups are not free of sharp edges. Three are worth knowing before you reorganize a large project:
- Resolution is global. Poetry resolves all groups together into one lock file. A version conflict inside an optional group can block
poetry lockfor everyone, even people who never install that group.--onlyisolates installation, not resolution. - Optional groups are strictly opt-in. A group marked
optional = trueis not part of the default set, sopoetry install --with testwill not pull it in. You must name it explicitly. Teams sometimes expect additive behavior and are surprised when docs dependencies silently vanish. - Reserved names.
mainrefers to your runtime dependencies; naming another groupmain(or colliding with the implicitdevmapping from legacy config) creates confusing behavior. Pick distinct, descriptive names.
Also note that Poetry 2.x changes some group semantics (including groups depending on other groups), so if you are pinning Poetry 1.x in CI, check the changelog before upgrading — the examples above assume the 1.2+ behavior.
Try it in ten minutes
The fastest way to build intuition: add a test group to an existing project, run poetry lock, then poetry install --only main in a fresh virtualenv and confirm with poetry show that pytest is absent. Then run poetry install --with test and watch it appear. Once you see the boundary hold, move your lint and docs dependencies into their own groups and update your CI stages to install only what each stage runs. Your production environment — and your pipeline clock — will thank you.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.