# Understanding dependency groups in uv


[uv](https://pydevtools.com/handbook/reference/uv.md) separates project dependencies into **dependency groups** (for development workflows) and **optional dependencies** (extras, for features shipped to users). Knowing which to use keeps your [pyproject.toml](https://pydevtools.com/handbook/reference/pyproject.toml.md) clean and your users' installs lean.

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

## Separate the tools from the product

The `[project]` table in pyproject.toml declares the packages that every user of a library or application needs:

```toml
[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

Dependency groups, defined in the `[dependency-groups]` table per [PEP 735](https://pydevtools.com/handbook/explanation/what-is-pep-735.md), are never published to [PyPI](https://pydevtools.com/handbook/explanation/what-is-pypi.md) and never installed when someone runs `pip install yourpackage`.

```toml
[dependency-groups]
dev = [
    "ruff>=0.15",
]
test = [
    "pytest>=9.0",
]
docs = [
    "sphinx>=9.1",
]
```

### Install packages into a group

```bash
uv add --group test pytest
uv add --group docs sphinx
uv add --group dev ruff
```

The `--dev` flag is a shorthand for `--group dev`:

```bash
uv add --dev ruff
# equivalent to: uv add --group dev ruff
```

### The 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.

```bash
uv sync          # installs dependencies + dev group
uv sync --no-dev # equivalent to --no-group dev; other default groups still install
```

### Control which groups get installed

The `--group`, `--only-group`, and `--no-group` flags give precise control over what gets installed:

```bash
# 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-groups
```

The 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:

```toml
[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`:

```toml
[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:

```toml
[project.optional-dependencies]
viz = [
    "matplotlib>=3.10",
]
```

Users install extras with bracket syntax:

```bash
pip install mypackage[viz]
uv pip install mypackage[viz]
```

### Manage extras with uv

```bash
# 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-extras
```

## Choose between groups and extras

Use a dependency group for someone working *on* the project. Use an extra for someone using *the project*.

Test dependencies belong in `[dependency-groups]`, not in a `test` extra under `[project.optional-dependencies]`. A `test` extra works mechanically, but it 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:

```toml
[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:

```toml
[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](https://peps.python.org/pep-0735/) 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

- [What are Optional Dependencies and Dependency Groups?](https://pydevtools.com/handbook/explanation/what-are-optional-dependencies-and-dependency-groups.md)
- [What is PEP 735?](https://pydevtools.com/handbook/explanation/what-is-pep-735.md)
- [uv: A Complete Guide](https://pydevtools.com/handbook/explanation/uv-complete-guide.md)
- [PEP 735 specification](https://peps.python.org/pep-0735/)
- [uv dependency groups documentation](https://docs.astral.sh/uv/concepts/dependencies/#dependency-groups)
