Skip to content

Set up Ruff for formatting and checking your code

This tutorial sets up Ruff in a uv project: format your Python code, check it for errors, keep the settings in pyproject.toml, and run the whole thing on every commit.

Set up Ruff in 30 seconds

Inside an existing project, four commands get you a working setup:

uv add --dev ruff           # install Ruff as a development dependency
uv run ruff check .         # report lint errors
uv run ruff check --fix .   # apply the fixes Ruff can make safely
uv run ruff format .        # reformat every Python file

That is the whole daily loop. The rest of this tutorial builds it from an empty project, explains what each command prints, and adds configuration and a pre-commit hook.

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.

Create a sample project

Create a new project to demonstrate Ruff:

$ uv init --no-package 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. --no-package keeps the project layout flat (a main.py at the top level instead of a src/ package). 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}! Ruff checks this file for lint errors and formats it consistently.')
    unused_var = 42

if __name__=='__main__':
    hello()

This code has several problems Ruff can find:

  • Unsorted and poorly formatted imports
  • Imports that are never used
  • An unused local variable
  • Missing whitespace around operators
  • Single quotes where the formatter wants double

Install Ruff into the project

The --dev flag marks Ruff as a development dependencyA package needed during development (testing, linting, formatting) that is not shipped to users of your project. Installed with the --dev flag. , keeping it out of production installs:

$ uv add --dev ruff
Using CPython 3.14.6
Creating virtual environment at: .venv
Resolved 2 packages in 3ms
Installed 1 package in 1ms
 + ruff==0.16.2

Your exact Python and Ruff versions may differ. Notice the new .venv/ directory: uv add created the project’s virtual environmentAn isolated folder where Python installs packages for one project, keeping them separate from other projects and your system Python. Learn more → because this is the first dependencyAn external package your project needs, listed in pyproject.toml so tools can install it automatically. . Future uv commands reuse it instead of touching your system Python.

Configure Ruff in pyproject.toml

Ruff’s default rule set already covers Pyflakes, flake8-bugbear, pyupgrade, flake8-simplify, import sorting, and more than 30 other rule families, so a new project is useful with no configuration at all. Configuration is for the settings you want to differ from those defaults.

Open the pyproject.toml file and add this to the bottom:

[tool.ruff]
line-length = 100

line-length sets the column the formatter wraps at, and the ceiling the E501 lint rule uses if you enable it. Ruff’s default is 88; raising it to 100 is the difference between the print line in main.py (95 characters) staying on one line and being split across three.

Settings are grouped by what they affect:

Table Controls Example setting
[tool.ruff] Both the linter and the formatter line-length, target-version, exclude
[tool.ruff.lint] Which rules run extend-select, ignore, per-file-ignores
[tool.ruff.format] How code is laid out quote-style, indent-style, docstring-code-format

Keeping these in pyproject.toml means the project’s dependencies, its build metadata, and its lint policy live in one file that uv add, uv run, and Ruff all read. Ruff also accepts a standalone ruff.toml or .ruff.toml, which is worth reaching for only when a directory has no pyproject.toml to put the settings in.

To enable rule families beyond the defaults, see how to configure recommended Ruff defaults.

Check your code with ruff check

uv run executes the command inside the project’s virtual environment, ensuring all dependencies are installed first:

$ uv run ruff check .
I001 [*] Import block is un-sorted or un-formatted
 --> main.py:1:1
  |
1 | / import sys,os
2 | | from pathlib    import Path
3 | | import json
  | |___________^
4 |
5 |   def hello(name:str='World'):
  |
help: Organize imports
  |
  - import sys,os
  - from pathlib    import Path
1 | import json
2 + import os
3 + import sys
4 + from pathlib import Path
5 +
6 |
  |

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

... (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).

Each diagnostic gives you the rule code, the offending span, a help: line, and a preview of the edit Ruff would make. The [*] marker means Ruff can apply that edit for you. Five of the six carry it; the sixth sits behind --unsafe-fixes because deleting an assignment can change what the program does.

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}! Ruff checks this file for lint errors and formats it consistently.')
4 |     unused_var = 42
  |     ^^^^^^^^^^
5 |
6 | if __name__=='__main__':
  |
help: Remove assignment to unused variable `unused_var`

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

The count drops from six to five because Ruff re-checks the file as it edits: removing the four unused imports also empties the import block, so I001 resolves without a separate fix of its own. Open main.py and you will see all four imports gone, because nothing referenced them. The unused_var = 42 line stayed:

main.py

def hello(name:str='World'):
    print(f'Hello, {name}! Ruff checks this file for lint errors and formats it consistently.')
    unused_var = 42

if __name__=='__main__':
    hello()

Spacing and quotes still look messy. That is the formatter’s job, not the linter’s.

Tell ruff check and ruff format apart

Ruff ships two commands that people often expect to be one:

ruff check ruff format
Job Finds problems Rewrites layout
Catches Unused imports, unsorted imports, bugs, outdated syntax Whitespace, quote style, line breaks, trailing commas
Changes behavior Can, when a fix removes code Never
Writes to disk Only with --fix Always, unless --check or --diff
Replaces flake8, isort, pyupgrade Black
Configured under [tool.ruff.lint] [tool.ruff.format]

Run the linter first and the formatter second. A lint fix can delete an import or collapse a statement, and the formatter then lays out whatever is left.

Format your code with ruff format

$ uv run ruff format .
1 file reformatted, 1 file left unchanged

The unchanged file is pyproject.toml; Ruff reads it for [project] metadata rules and counts it alongside the Python files.

Reopen main.py. 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}! Ruff checks this file for lint errors and formats it consistently.")
    unused_var = 42


if __name__ == "__main__":
    hello()

The print line is 95 characters and survives intact because of the line-length = 100 setting added earlier. Drop that setting and the formatter falls back to 88 columns, splitting the call across three lines.

Ruff’s formatter is deterministic: it produces the same output regardless of the input formatting, helping maintain a consistent style across your project.

One diagnostic is still outstanding. Delete the unused_var = 42 line yourself, then confirm the file is clean:

$ uv run ruff check .
All checks passed!

Check formatting without rewriting files

On a build server you want to know whether code is formatted, not to have it quietly rewritten. --check reports and exits non-zero instead of editing:

$ uv run ruff format --check .
2 files already formatted

Add a sloppy function to the end of main.py to watch it fail:

main.py
def  bad( x ):
    return   x
$ uv run ruff format --check .
unformatted: File would be reformatted
  --> main.py:9:5
   |
8  |
   - def  bad( x ):
   -     return   x
9  + def bad(x):
10 +     return x
   |

1 file would be reformatted, 1 file already formatted

The command exits with status 1, which fails the CI job. Run uv run ruff format . to clean it up again. Pair --check with uv run ruff check . (no --fix) to get the lint half of the same gate; see setting up GitHub Actions with uv for the full workflow.

Run Ruff before every commit

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

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

Keep rev in step with the Ruff version in pyproject.toml, or the hook and your local runs will disagree about what counts as formatted.

Install pre-commit and the hooks:

$ uvx pre-commit install
Installed 10 packages in 19ms
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.

Check the hooks against the whole project without waiting for a commit:

$ uvx pre-commit run --all-files
ruff check...............................................................Passed
ruff format..............................................................Passed

From here, every git commit lints and formats the staged files first.

Last updated on