skip to content

Why did PEP 735 add dependency groups when pyproject.toml already had extras?

level: middleimportance: nice to knowfreq 22%

answer

  1. One is published, the other stays local
  2. Not everything with a pyproject.toml is a package
  3. A top-level table, not under [project]
  4. include-group composes them
  5. pip --group, added in 25.1

basics

~20 s

Extras are published metadata on a distribution, so a dev extra ships to every consumer and only works if the project is a package at all. PEP 735 dependency groups live in a top-level [dependency-groups] table, stay in the source tree, and never reach the built wheel.

solid answer

~40 s

Extras were the only standard place to park test and lint requirements, and they fit badly. An extra is metadata *about the distribution*: it is built into the wheel, published to the index, installable by anyone as `pkg[dev]`, and it requires the project to be a package with a `[project]` table and a build backend — which a service or pipeline repo often is not. Installing a dev extra also forces an install of your own project. PEP 735 (2024) added a top-level `[dependency-groups]` table: keys are group names, values are requirement strings plus `{include-group = "other"}` entries for reuse. Groups are **local to the source tree**, absent from built metadata, and usable with no `[project]` table at all. pip installs one with `pip install --group dev` (25.1+).

code

python · 12 lines
python
import tomllib

pyproject = '''
[dependency-groups]
test = ["a-test-runner>=8"]
lint = ["a-style-checker", "a-type-checker"]
dev = [{include-group = "test"}, {include-group = "lint"}, "a-debugger"]
'''

groups = tomllib.loads(pyproject)["dependency-groups"]
print(sorted(groups))
print(groups["dev"])

go deeper

for a junior

You are not expected to have used this yet. Recall the one-line difference: an extra is published with the package and anyone can install it, a dependency group stays in the repository and is meant for development tooling.

for a middle

Be ready to place the table correctly — [dependency-groups] is top-level, not under [project] — and to name the composition entry {include-group = "..."} and the install command pip install --group dev.

for a senior

Show the reasoning that motivated the PEP: published metadata leaking dev tooling to consumers, non-package repositories having nowhere standard to write requirements, and the wasted self-install that pip install -e ".[dev]" forces in CI.

for a principal

Own the migration and tooling-support question. Adopting groups means every installer in your build, image and CI paths must understand them, and you must decide what still legitimately belongs in a published extra versus what becomes repository-private.

### The gap extras left Before PEP 735 there were exactly two standard-ish places to write down "the packages I need to *work on* this project": a `requirements-dev.txt` file, which is a pip-specific convention with no metadata standard behind it, or an extra named `dev`, `test` or `lint`. Both were compromises, and the extra was the worse one for four concrete reasons. **An extra is published metadata.** Everything under `[project.optional-dependencies]` is compiled into the wheel's `METADATA` as `Provides-Extra` and marker-guarded `Requires-Dist` lines. Your test runner, your type checker and your coverage plugin therefore appear on the index page of a library that has nothing to do with them, and any consumer can install them with `pip install yourlib[dev]`. They also enter the dependency graph of your distribution, so their version constraints can participate in a downstream resolution that has no business seeing them. **An extra requires you to be a package.** Extras live inside the `[project]` table, which is build-backend metadata. A repository that is an application, a service or a batch pipeline — no wheel, no publication, nothing importable by anyone else — had no legitimate way to declare its development requirements in `pyproject.toml` at all. It had to invent a build backend just to get a place to write them down. **Installing an extra installs your project.** `pip install -e ".[dev]"` builds and installs your own distribution as a side effect of fetching a linter. In CI that is wasted work at best, and at worst it fails for an unrelated reason (a missing compiler, a build backend that cannot run on the image) when all you wanted was the tools. **Extras cannot be composed.** There is a self-referential trick (`all = ["mypkg[test,lint]"]`) but it only works because the project is installable, and it drags the project install along with it. ### What a dependency group is PEP 735 defines a **top level** table — note that it is *not* nested under `[project]`: ```toml [dependency-groups] test = ["a-test-runner>=8"] lint = ["a-style-checker", "a-type-checker"] dev = [{include-group = "test"}, {include-group = "lint"}, "a-debugger"] ``` Each key is a group name (normalized like a package name), and each value is a list whose entries are either requirement strings or inline tables of the form `{include-group = "other"}`. Inclusion is how groups compose: `dev` above expands to the union of `test`, `lint` and its own entries, with cycles forbidden. The defining property is scope. A dependency group is **source-tree metadata**. It is not copied into the wheel or the sdist's built metadata, no consumer can install it from the index, and it does not participate in the resolution of your distribution for anyone else. It also does not require a `[project]` table or a `[build-system]` table: a `pyproject.toml` containing nothing but `[dependency-groups]` is valid and useful, which is what finally gave non-package repositories a standard home for their tooling. ### Installing them pip gained `--group` in version 25.1 (2025): `python -m pip install --group dev` reads `pyproject.toml` in the current directory and installs that group; the longer form `--group path/to/pyproject.toml:dev` points elsewhere. Other installers read the same table natively — uv resolves groups as part of its project workflow and treats `dev` specially. An installer that predates the PEP simply never looks at the table. ### What they are not A dependency group is **not a lockfile**. It holds requirement specifiers, exactly like `[project] dependencies` does, and it is resolved fresh at install time; reproducible installs are still the job of a lock or constraint mechanism. It is also not a replacement for extras. The two answer different questions. An extra says *"consumers of this library may opt into this feature"* and is therefore public and published. A group says *"people working in this checkout need these tools"* and is therefore private and local. The decision rule is simply: if someone installing your package from an index would ever want it, it is an extra; if it only makes sense inside the repository, it is a group. ### The migration Moving from a `dev` extra to a `dev` group is mostly mechanical — copy the list, drop the `-e .` self-install if your test command does not actually need the package importable, and update CI. The one thing to check is anything that relied on the dev extra being installable from the index, because that capability is deliberately gone.

  • Can a consumer install your `dev` dependency group from the index?
    No. Dependency groups are not written into the wheel's or sdist's built metadata, so there is nothing on the index to install — `pip install yourlib[dev]` would look for an *extra* by that name and warn that it does not exist. Groups are only reachable from a checkout that contains the `pyproject.toml` declaring them. That privacy is the whole point of the mechanism.
  • Does a repository need a `[project]` table or a build backend to use dependency groups?
    No, and that is one of the main motivations. `[dependency-groups]` is a top-level table with no dependency on `[project]` or `[build-system]`, so an application, service or pipeline repository that never builds a wheel can still declare its tooling in `pyproject.toml` in a standard way. Previously such a repository had to fall back on an unstandardized requirements file.
  • Do dependency groups give you reproducible installs?
    No. A group holds requirement specifiers that are resolved fresh at install time, exactly like base dependencies — `>=8` still picks whatever satisfies it today. Groups standardize *what you want*, not *what you got*; reproducibility remains the job of a lock file or a constraints file layered on top of the resolution.

saying these in an interview costs you the question

  • Says dependency groups ship in the wheel like extras
  • Puts [dependency-groups] inside the [project] table
  • Calls a dependency group a lockfile
  • Claims groups require a build backend to work
  • Thinks extras are deprecated by PEP 735
  • Says consumers can install a dev group from the index

context