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
Hide 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.