# Set up Ruff for formatting and checking your code

This tutorial helps you set up Ruff to automatically format your Python code and check it for common errors and style issues.

## Prerequisites

Before starting, make sure you have uv installed on your system. You can install it following the [installation guide](https://docs.astral.sh/uv/getting-started/installation/).

{{< callout type="info" >}}
You do not need Python installed - uv will handle installing it automatically.
{{< /callout >}}

## Creating a sample project

Let's create a new project to demonstrate Ruff:

```console
$ uv init ruff-demo
Initialized project `ruff-demo` at `/path/to/ruff-demo`
$ cd ruff-demo
```

If you see `error: command not found: uv`, finish the [installation guide](https://docs.astral.sh/uv/getting-started/installation/) and reopen your shell.

Notice the new `main.py` file alongside `pyproject.toml`, `.python-version`, and a fresh `.git/` directory. uv populates a starter `main.py` so the project is runnable from the first command. Open `main.py` and replace its contents with the following messy code:

```python {filename="main.py"}
import sys,os
from pathlib    import Path
import json

def hello(name:str='World'):
    print(f'Hello, {name}!')
    unused_var = 42

if __name__=='__main__':
    hello()
```

This code has several style issues that Ruff can help fix:

* Unsorted and poorly formatted imports
* Inconsistent spacing
* Unused variables
* Missing whitespace around operators

## Installing Ruff

Add Ruff as a development dependency of your project:

```console
$ uv add --dev ruff
Using CPython 3.14.4
Creating virtual environment at: .venv
Resolved 2 packages in 81ms
Installed 1 package in 3ms
 + ruff==0.15.12
```

Your exact Python and Ruff versions may differ. Notice the new `.venv/` directory: `uv add` created the project's virtual environment because this is the first dependency. Future `uv` commands reuse it instead of touching your system Python.

## Configuring Ruff

Open the [pyproject.toml](https://pydevtools.com/handbook/reference/pyproject.toml.md) file in your project and add this Ruff configuration to the bottom:

```toml
[tool.ruff.lint]
extend-select = ["B"]
```

By default, Ruff checks for a large number of syntax errors. This configuration extends the defaults with [additional checks (`B`)](https://docs.astral.sh/ruff/rules/#flake8-bugbear-b) for Python errors and gotchas.

## Running Ruff

Let's check our code with Ruff:

```console
$ uv run ruff check .
E401 [*] Multiple imports on one line
 --> main.py:1:1
  |
1 | import sys,os
  | ^^^^^^^^^^^^^
2 | from pathlib    import Path
3 | import json
  |
help: Split imports

F401 [*] `sys` imported but unused
 --> main.py:1:8
  |
1 | import sys,os
  |        ^^^
  |
help: Remove unused import

... (four more diagnostics: F401 for `os`, `pathlib.Path`, and `json`, plus F841 for `unused_var`)

Found 6 errors.
[*] 5 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
```

Ruff prints a multi-line annotated diagnostic for each issue, pointing at the offending span and suggesting a fix. Each block ends with a `help:` line. Notice the `[*]` marker on five of the six issues: it means Ruff can auto-fix them. The sixth is hidden behind `--unsafe-fixes` because removing an assignment can change runtime behavior.

Let's apply the safe fixes:

```console
$ uv run ruff check --fix .
F841 Local variable `unused_var` is assigned to but never used
 --> main.py:4:5
  |
2 | def hello(name:str='World'):
3 |     print(f'Hello, {name}!')
4 |     unused_var = 42
  |     ^^^^^^^^^^
  |
help: Remove assignment to unused variable `unused_var`

Found 6 errors (5 fixed, 1 remaining).
No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option).
```

Open `main.py` and notice that all four imports are gone. Ruff removed them because none of them were used; the lint and the auto-fix don't preserve dead imports. The `unused_var = 42` line stayed because that fix is gated behind `--unsafe-fixes`. The file now reads:

```python {filename="main.py"}
def hello(name:str='World'):
    print(f'Hello, {name}!')
    unused_var = 42

if __name__=='__main__':
    hello()
```

Spacing and quotes still look messy because `ruff check --fix` only applies lint fixes; formatting is a separate step.

## Using Ruff's Formatter

Ruff can also format your code.

Let's enable formatting:

```console
$ uv run ruff format .
1 file reformatted
```

Reopen `main.py` to see the result. Spacing around `=` and `==`, double quotes, and a blank line between the function and the `if __name__` block now match standard Python style:

```python {filename="main.py"}
def hello(name: str = "World"):
    print(f"Hello, {name}!")
    unused_var = 42


if __name__ == "__main__":
    hello()
```

{{< callout type="info" >}}
Ruff's formatter is deterministic: it produces the same output regardless of the input formatting, helping maintain a consistent style across your project.
{{< /callout >}}

## Adding Pre-commit Hooks

To automatically run Ruff before each Git commit, create a `.pre-commit-config.yaml` file:

```yaml
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
  rev: v0.15.15
  hooks:
    - id: ruff-check
      args: [ --fix ]
    - id: ruff-format
```

Install pre-commit and the hooks:

```console
$ uvx pre-commit install
Downloading virtualenv (7.2MiB)
 Downloaded virtualenv
Installed 10 packages in 9ms
pre-commit installed at .git/hooks/pre-commit
```

If you see `An error has occurred: InvalidConfigError: ... is not a git repository`, the project is missing a `.git/` directory. `uv init` creates one automatically, so this only happens if you started outside a project root or deleted `.git/`. Run `git init` and try again.

Now Ruff will automatically check and format your code whenever you commit changes!

## Next Steps

You've successfully set up Ruff for code formatting and linting. To learn more:

* [Ruff: a complete guide](https://pydevtools.com/handbook/explanation/ruff-complete-guide.md) for the full picture of rule categories, configuration, and migration
* [How to configure recommended Ruff defaults](https://pydevtools.com/handbook/how-to/how-to-configure-recommended-ruff-defaults.md) for a more comprehensive set of linting rules
* [How to sort Python imports with Ruff](https://pydevtools.com/handbook/how-to/how-to-sort-python-imports-with-ruff.md)
* [Ruff reference page](https://pydevtools.com/handbook/reference/ruff.md) for a full overview of Ruff's capabilities
* Explore [additional Ruff rules](https://docs.astral.sh/ruff/rules/) to enable
* Configure [Ruff in your editor](https://docs.astral.sh/ruff/editors/setup/)
