# pyproject-fmt: Opinionated pyproject.toml Formatter


pyproject-fmt is an opinionated formatter for [`pyproject.toml`](https://pydevtools.com/handbook/reference/pyproject.toml.md) files. It applies packaging-aware rules that a general TOML formatter cannot: sorting dependency lists, normalizing version specifiers and package names, reordering tables into the canonical [PEP 621 project metadata](https://pydevtools.com/handbook/explanation/what-is-pep-621-compatibility.md) order, and generating Python version classifiers from `requires-python`. It is maintained by the tox-dev organization in the `toml-fmt` monorepo.

{{< callout type="info" >}}
pyproject-fmt only formats `pyproject.toml`. For arbitrary TOML files, use a general formatter like [taplo](https://pydevtools.com/handbook/how-to/how-to-format-pyproject-toml-with-taplo.md).
{{< /callout >}}

## When to use pyproject-fmt

Use pyproject-fmt when a project's `pyproject.toml` should follow one packaging-aware style without per-project debate. It takes the same opinionated stance as [Ruff](https://pydevtools.com/handbook/reference/ruff.md)'s formatter or Black: a small set of options and a single canonical output, which keeps diffs small and reviews focused. It complements rather than replaces a code formatter, since it touches only the project manifest and leaves Python source untouched.

## Formatting rules

pyproject-fmt rewrites `pyproject.toml` according to fixed rules:

* Reorders top-level tables so `[build-system]` precedes `[project]`, with `[tool.*]` tables after.
* Reorders keys inside `[project]` into the canonical PEP 621 order (`name`, `version`, `description`, `requires-python`, `classifiers`, `dependencies`, and so on).
* Sorts `dependencies`, `optional-dependencies`, and dependency groups case-insensitively.
* Normalizes version specifiers by trimming redundant trailing zeros (`requests>=2.0.0` becomes `requests>=2`).
* Normalizes distribution names to their [PEP 503](https://pydevtools.com/handbook/explanation/what-is-pep-503.md) canonical form (`Flask` becomes `flask`).
* Generates `Programming Language :: Python :: 3.x` classifiers from `requires-python`, up to the latest supported version.
* Wraps arrays and long strings at the configured column width and indents with two spaces.
* Preserves comments in place.

Given this input:

```toml
[project]
name = "demo"
dependencies = ["requests>=2.0.0", "click", "Flask>=3.0.0"]
version = "0.1.0"
requires-python = ">=3.10"
description = "x"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
```

pyproject-fmt produces:

```toml
[build-system]
build-backend = "hatchling.build"
requires = [ "hatchling" ]

[project]
name = "demo"
version = "0.1.0"
description = "x"
requires-python = ">=3.10"
classifiers = [
  "Programming Language :: Python :: 3 :: Only",
  "Programming Language :: Python :: 3.10",
  "Programming Language :: Python :: 3.11",
  "Programming Language :: Python :: 3.12",
  "Programming Language :: Python :: 3.13",
  "Programming Language :: Python :: 3.14",
]
dependencies = [ "click", "flask>=3", "requests>=2" ]
```

## Configuration

pyproject-fmt exposes a small set of options, settable on the command line or in a `[tool.pyproject-fmt]` table:

| Option | CLI flag | Default | Effect |
|---|---|---|---|
| `column_width` | `--column-width` | `120` | Width at which arrays split across lines and strings wrap |
| `indent` | `--indent` | `2` | Number of spaces per indentation level |
| `keep_full_version` | `--keep-full-version` | `false` | Keep redundant `.0` digits instead of trimming them |
| (classifier generation) | `--no-generate-python-version-classifiers` | on | Disable auto-generated Python version classifiers |
| `max_supported_python` | `--max-supported-python` | latest stable CPython | Highest Python minor version used when generating classifiers |

A shared configuration file can be passed with `--config path/to/pyproject-fmt.toml` to apply the same settings across multiple projects.

## Installation

```bash
# Using uv (recommended)
uv tool install pyproject-fmt

# Using pipx
pipx install pyproject-fmt

# Using pip
pip install pyproject-fmt
```

pyproject-fmt requires Python 3.10 or newer. Installing in an isolated environment with [uv](https://pydevtools.com/handbook/reference/uv.md) or pipx avoids conflicts with project dependencies.

## Usage

```bash
# Format pyproject.toml in place
pyproject-fmt pyproject.toml

# Print the formatted output instead of writing the file
pyproject-fmt --stdout pyproject.toml

# Check formatting without modifying files (useful in CI)
pyproject-fmt --check pyproject.toml

# Run once without installing
uvx pyproject-fmt pyproject.toml
```

The command returns a non-zero exit code when a file would be reformatted, so `--check` fails CI on unformatted input.

### Pre-commit hook

```yaml
repos:
  - repo: https://github.com/tox-dev/pyproject-fmt
    rev: v2.23.0
    hooks:
      - id: pyproject-fmt
```

For setup details, see [How to set up pre-commit hooks for a Python project](https://pydevtools.com/handbook/how-to/how-to-set-up-pre-commit-hooks-for-a-python-project.md).

## Pros

* Packaging-aware: sorts dependencies and generates classifiers, which a general TOML formatter cannot do
* Opinionated and low-configuration, producing consistent diffs across projects
* Preserves comments
* Runs as a CLI, a Python module, or a pre-commit hook

## Cons

* Formats only `pyproject.toml`, not other TOML files
* Automatic classifier generation and key reordering can produce large diffs on first run
* The opinionated output is intentional but not configurable to a house style beyond the few exposed options

## Learn More

* [How to format pyproject.toml with taplo](https://pydevtools.com/handbook/how-to/how-to-format-pyproject-toml-with-taplo.md)
* [pyproject.toml reference](https://pydevtools.com/handbook/reference/pyproject.toml.md)
* [pyproject-fmt documentation](https://pyproject-fmt.readthedocs.io/)
* [GitHub repository](https://github.com/tox-dev/toml-fmt)
* [pyproject-fmt on PyPI](https://pypi.org/project/pyproject-fmt/)
