Skip to content
← All Python articles

Python explainer

Group Dev, Test and Optional Dependencies

About 4 min to read. Allow extra time to work through examples.

Put optional features in extras, test and lint tools in dependency groups, and pip recipes in requirements files. See how one group includes another, and how pip and uv install a group.

This article uses Python. You can make it your practice mode to browse related content across the site.

Your project needs pytest to run its tests, mypy to check types and Sphinx to build the docs. Some users also want PostgreSQL support, which needs psycopg. None of these belong in [project].dependencies, because every user installs that list. Python packaging gives you other places to put them:

Purpose Where it goes
Needed by the installed code [project].dependencies
An optional feature that users can install [project.optional-dependencies], as an extra
Work on the project, such as tests or linting [dependency-groups]
An install recipe for pip A requirements file

An extra is an optional feature

[project.optional-dependencies]
postgres = ["psycopg>=3.2"]

An extra is part of the package's published metadata. A user who wants it asks for it by name, as in pip install mypkg[postgres]. Everyone else gets the package without psycopg. An extra called all that bundles every optional feature is fine too. Older projects often have a dev extra for their test tools. It works, but it publishes those tools in the package's metadata, and a dependency group now does that job.

A dependency group is for work on the project

[dependency-groups]
test = ["pytest>=8", "pytest-cov"]
typing = ["mypy>=2", "types-requests"]
lint = ["ruff"]
docs = ["sphinx", "furo"]
dev = [
    {include-group = "test"},
    {include-group = "typing"},
    {include-group = "lint"},
    {include-group = "docs"},
]

A dependency group is a named list of packages for people who work on the project. The wheel's metadata leaves groups out, so users never install them. They are not secret, though, because the sdist still contains pyproject.toml.

The dev group combines the others. An entry such as {include-group = "test"} inserts the test group's entries at that point, and expands any includes inside it in turn. Repeated requirements stay:

[dependency-groups]
base = ["coverage"]
test = ["pytest", {include-group = "base"}]
check = [{include-group = "base"}, {include-group = "test"}]

Expanding check gives ["coverage", "pytest", "coverage"]. Expanding is not installing. The installer resolves the whole list afterwards, so it sees both copies of coverage and installs one. If the list holds demo>2 and demo<1, the resolver reports the conflict. The expansion must keep both lines and leave the decision to the resolver.

A group that includes itself, directly or through other groups, is an error. The standard compares group names after normalizing them, so Test, test and TEST name one group. The exercise below gives you names that are already normalized. See the dependency group specification.

A requirements file is pip's own format

-r base.txt
-c constraints.txt
httpx==0.28.1

A requirements file lists one requirement per line, and it can carry pip options. Here -r pulls in another requirements file, and -c applies a constraints file, which limits versions without adding packages. Tools such as pip freeze and uv pip compile write requirements files for you, with every version pinned. See the requirements file format.

Install a group

pip installs a group with python -m pip install --group test. That installs only the group, not your project. Add the project when the job needs it, as in python -m pip install -e . --group test.

uv sync installs the dev group by default. --group NAME adds another group. --only-group lint installs only that group, without your project or its dependencies, which suits a job that only runs Ruff. uv locks every group together in one uv.lock. So two groups that need conflicting versions can make uv lock fail, even if no job installs both. uv's conflicts setting handles that case. See uv dependency groups.

Check your understanding A library has an optional PostgreSQL backend. Where do postgres, test, typing and docs go? Show answer

postgres is an extra, because it is an optional feature that users of the installed library ask for. test, typing and docs are groups, because only the people who work on the library need them.

Then check what each CI job imports. Tests usually need the project and its dependencies installed too, and a type check may need the stubs of your dependencies. Use --only-group only for a job, such as linting, that needs nothing but the tool.

step 29 of 41

Modules, Packaging and Tooling

Up next Expand a Dependency Group into Its Requirements