Skip to content

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.

You do not need Python installed - uv will handle installing it automatically.

Creating a sample project

Let’s create a new project to demonstrate Ruff:

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

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:

$ 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 file in your project and add this Ruff configuration to the bottom:

[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) for Python errors and gotchas.

Running Ruff

Let’s check our code with Ruff:

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

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

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:

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

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


if __name__ == "__main__":
    hello()
Ruff’s formatter is deterministic: it produces the same output regardless of the input formatting, helping maintain a consistent style across your project.

Adding Pre-commit Hooks

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

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

Install pre-commit and the hooks:

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

Last updated on