Understanding dependency groups in uv
A Python project’s dependencies fall into two categories: the packages end users need, and the packages developers need. Production code requires requests; running the test suite requires pytest. These two sets of dependencies serve different audiences and belong in different places in pyproject.toml.
uv provides two mechanisms for managing these separate sets: dependency groups for development workflows and optional dependencies (extras) for features shipped to users. Understanding when to reach for each one prevents a common source of project configuration mistakes.
Separate the tools from the product
The [project] table in pyproject.toml declares the packages that every user of a library or application needs:
[project]
name = "myproject"
dependencies = [
"httpx>=0.28",
"pydantic>=2.0",
]But developers working on the project also need linters, test runners, and documentation builders. Those tools should never appear in dependencies because they would be installed on every user’s machine for no reason.
Three places in pyproject.toml can hold these non-production requirements. Each serves a different purpose.
Use dependency groups for development workflows
The [dependency-groups] table, standardized by PEP 735, organizes dependencies by development task. Groups are never published to PyPI and never installed when someone runs pip install yourpackage.
[dependency-groups]
dev = [
"ruff>=0.15",
]
test = [
"pytest>=9.0",
]
docs = [
"sphinx>=9.1",
]Add packages to a group
uv add --group test pytest
uv add --group docs sphinx
uv add --group dev ruffThe --dev flag is a shorthand for --group dev:
uv add --dev ruff
# equivalent to: uv add --group dev ruffThe dev group has special status
Running uv sync with no flags installs project dependencies plus the dev group. No other group is included by default. This makes dev the natural home for tools used during everyday development, like linters and formatters.
uv sync # installs dependencies + dev group
uv sync --no-dev # equivalent to --no-group dev; other default groups still installControl which groups get installed
The --group, --only-group, and --no-group flags give precise control over what gets installed:
# Install dependencies + dev group + test group
uv sync --group test
# Install ONLY the test group (no project dependencies, no dev group)
uv sync --only-group test
# Install dependencies + dev + test + docs
uv sync --group test --group docs
# Install dependencies + all groups
uv sync --all-groups
# Skip the dev group (other default groups still install)
uv sync --no-dev
# Skip ALL default groups
uv sync --no-default-groupsThe distinction between --group and --only-group matters in CI. A test job that needs pytest but not the project’s own code can use --only-group test for a faster, leaner install.
Make additional groups install by default
If the dev group alone is not enough, the [tool.uv] table accepts a default-groups setting:
[tool.uv]
default-groups = ["dev", "test"]With this configuration, uv sync installs both groups without extra flags. The --no-default-groups flag overrides this behavior.
Include one group inside another
A group can pull in another group using the include-group directive. This avoids duplication when a broad group like dev should contain everything from test and lint:
[dependency-groups]
test = ["pytest>=9.0"]
lint = ["ruff>=0.15"]
dev = [
{include-group = "test"},
{include-group = "lint"},
]Running uv sync (which installs the dev group by default) now installs both pytest and ruff.
Use optional dependencies for end-user features
Optional dependencies, defined under [project.optional-dependencies], are published to PyPI. They let users opt in to features that require additional packages:
[project.optional-dependencies]
viz = [
"matplotlib>=3.10",
]Users install extras with bracket syntax:
pip install mypackage[viz]
uv pip install mypackage[viz]Manage extras with uv
# Add a package to an extra
uv add --optional viz matplotlib
# Sync with a specific extra enabled
uv sync --extra viz
# Sync with all extras
uv sync --all-extrasChoose between groups and extras
The decision comes down to audience. If the dependency is for someone working on the project, use a group. If it is for someone using the project, use an extra.
| Dependency groups | Optional dependencies | |
|---|---|---|
| Audience | Developers of the project | Users of the project |
| Published to PyPI | No | Yes |
| pyproject.toml section | [dependency-groups] | [project.optional-dependencies] |
| Install syntax | uv sync --group test | uv sync --extra viz |
| Typical contents | pytest, ruff, sphinx | matplotlib, boto3 |
A common mistake is putting test dependencies in [project.optional-dependencies] under a test extra. This works mechanically but publishes internal tooling choices to every user who inspects the package metadata. Dependency groups keep development concerns private.
Handle conflicting dependencies between groups
Sometimes two groups need incompatible versions of the same package. A project might pin one version of numpy for a legacy compatibility group and a newer version for the main test suite. By default, uv resolves all groups together, which would fail if two groups demand conflicting numpy versions.
The [tool.uv] table provides a conflicts setting to declare that certain groups should never be installed together:
[dependency-groups]
compat = ["numpy>=1.26,<2.0"]
modern = ["numpy>=2.1"]
[tool.uv]
conflicts = [
[
{group = "compat"},
{group = "modern"},
],
]With this declaration, uv resolves each conflicting group independently. Running uv sync --group compat and uv sync --group modern each produce a valid environment, but attempting to install both at once would raise an error.
Move away from the legacy approach
Before PEP 735 and [dependency-groups], uv stored development dependencies in a tool-specific table:
[tool.uv]
dev-dependencies = [
"pytest>=9.0",
"ruff>=0.15",
]This still works. When both tool.uv.dev-dependencies and dependency-groups.dev exist, uv combines them. If the legacy field is present, uv add --dev continues writing to it rather than to [dependency-groups]. New projects should use [dependency-groups] exclusively, and existing projects should migrate by moving entries from tool.uv.dev-dependencies into dependency-groups.dev.
PEP 735 in context
PEP 735 was accepted in October 2024 to fill a gap in the Python packaging standards. Before it, there was no standard place for development dependencies; each tool invented its own (Poetry used [tool.poetry.dev-dependencies], uv used [tool.uv.dev-dependencies], and PDM used [tool.pdm.dev-dependencies]). PEP 735 gives the ecosystem a single [dependency-groups] table that any tool can read, making project configuration portable across package managers.
Further reading
Get Python tooling updates
Subscribe to the newsletter