Quick Start Guide to Ruff, a Python Linter and Formatter

Introduction
A couple of weeks ago, I presented at my local Python meetup on Ruff, a Python linter and formatter written in Rust by Astral. My last blog post was on the uv package manager, also by Astral, and the presentation inspired me to write about it. This time, I would like to write a quick start guide to ruff so you can get started formatting and linting your Python code.
What's the Difference Between a Linter and Formatter?
A linter is a code analysis tool that finds errors and possible bugs without running the program itself. Some errors can be fixed automatically by the tool, but most times it is better for the programmer to read through, understand, and fix the error themselves.
On the other hand, a code formatter automatically formats your code according to a set guideline. Well-formatted code is easier for developers to read, review, and maintain. PEP 8 - Style Guide for Python Code is THE style guide for Python that outlines indentations (4 spaces per indentation), spacing rules, line breaks, commenting, and even when to use a trailing comma.
Both linter and formatter are important developer tools in order to follow good coding practices and not get on other developer's nerves.
Python Linters and Formatters Consolidated into Ruff
Several Python packages have become standards in the Python ecosystem:
flake8 is both linter and formatter that is a wrapper around several other tools (pyflakes, pycodestyle, and McCabe complexity checker)
Black is the "uncompromising Python code formatter" that reduced arguments over code style
isort is a package that sorts and organizes imports
All of these packages are incredibly helpful, but managing multiple tools can be rough difficult (I know, bad joke). Ruff re-implemented the functionality and logic behind all these tools in Rust to become a extremely fast linter and formatter. Instead of running multiple tools, ruff can do it all.
Add Ruff to your Project
You can install ruff with your preferred package manager, but I recommend using uv.
With uv, you can add ruff as a development dependency or as a tool. As a dev dependency, ruff will only be available within the project's virtual environment. As a uv tool, it can be available to all projects managed by uv.
If you are working together with other developers on a shared repository, it's generally better to add ruff as a dev dependency to ensure consistency. If you're working solo with many other projects, installing as a tool may be more convenient.
# Add as a dev dependency with uv
uv add --dev ruff
# Add as a uv tool
uv tool install ruff
# Install with pip
pip install ruff
Use Ruff as a Linter
Running the linter is extremely easy. The command ruff check
will check for errors on all Python files within the current directory and its sub-directories. However, if there's many files and many errors, it will be more manageable to check a directory with a few Python files or a single file.
# Checks all Python files in current directory and subdirectories
ruff check
# Checks all Python files in specific file directory
ruff check <file_directory>
# Checks specific file
ruff check <specific_file_path>
# Fixes simple linting issue if possible
ruff check --fix
# NOTE: If using uv, add "uv run" before each command
# If using uv tool, add "uvx", which is equivalent to "uv tool run"
Output Example with ruff check
Let's look at the output of ruff check
with a simple example. The code example below has two functions, where calculate_value
sums two integer values and process_data
doubles each integer in a given array of integers.
import os
import json
def calculate_value(x: int, y: int) -> int:
"""Calculate a value."""
z = x + y
temp_value = x * 2
return z
def process_data(data_list: List[int]) -> List[int]:
results = []
for i in range(len(data_list)):
item = data_list[i]
if item != None:
results.append(item * 2)
return results
process_data([1, 2, 3])
Running ruff check file/path/<file_name>
shows the following errors. Three different errors are noted with a code and explanation:
I001 -- Import block is unsorted or un-formatted.
F841 -- Local variable is assigned but never used.
E711 -- Comparison to 'None' should be 'condition is not None.'
These error codes can be found in the Rules section of the documentation and becomes handy when you want to configure ruff to follow or ignore certain rules.
$ uv run ruff check src/check_example.py
src\check_example.py:1:1: I001 [*] Import block is un-sorted or un-formatted
|
1 | / import os
2 | | import json
| |___________^ I001
3 |
4 | def calculate_value(x, y):
|
= help: Organize imports
src\check_example.py:7:5: F841 Local variable `temp_value` is assigned to but never used
|
5 | """Calculate a value."""
6 | z = x + y
7 | temp_value = x * 2
| ^^^^^^^^^^ F841
8 | return z
|
= help: Remove assignment to unused variable `temp_value`
src\check_example.py:15:20: E711 Comparison to `None` should be `cond is not None`
|
13 | for i in range(len(data_list)):
14 | item = data_list[i]
15 | if item != None:
| ^^^^ E711
16 | results.append(item * 2)
|
= help: Replace with `cond is not None`
Found 3 errors.
[*] 1 fixable with the `--fix` option (2 hidden fixes can be enabled with the `--unsafe-fixes` option).
At the very bottom of the output, you'll notice the message of how many errors were detected and how many are fixable with the --fix
flag.
After running ruff check --fix file/path/<file_name>
for this specific example, the import statements were organized alphabetically. However, the two other errors were not automatically fixed. It's recommended that you fix it yourself instead of relying on the --unsafe-fixes
flag.
After fixing these errors and re-running ruff check
, the message "All checks passed!" should show up in your terminal.
"""
Fixed code --> see what I changed in the comments below.
"""
# import statements organized alphabetically
import json
import os
def calculate_value(x: int, y: int) -> int:
"""Calculate a value."""
z = x + y
# removed temp_value
return z
def process_data(data_list: List[int]) -> List[int]:
results = []
for i in range(len(data_list)):
item = data_list[i]
if item is not None: # fixed '!=' to 'is not'
results.append(item * 2)
return results
process_data([1, 2, 3])
Use Ruff as a Formatter
Running the formatter is also extremely easy. A simple command of ruff format
will format all Python files within the current directory. Again, if formatting a large codebase, it's better to format in small batches and check for unintended changes from happening.
Ruff follows Black's formatting rules.
# Formats all Python files in current directory and subdirectories
ruff format
# Formats all Python files in file directory
ruff format <file_directory>
# Formats specific Python file
ruff format <specific_file_path>
# NOTE: If using uv, add "uv run" before each command
# If using uv tool, add "uvx", which is equivalent to "uv tool run"
Output Example with ruff format
This example code is from the Black Playground. You'll notice this code is very difficult to read, but it is still valid Python code.
from seven_dwwarfs import Grumpy, Happy, Sleepy, Bashful, Sneezy, Dopey, Doc
x = { 'a':37,'b':42,
'c':927}
x = 123456789.123456789E123456789
if very_long_variable_name is not None and \
very_long_variable_name.field > 0 or \
very_long_variable_name.is_debug:
z = 'hello '+'world'
else:
world = 'world'
a = 'hello {}'.format(world)
f = rf'hello {world}'
if (this
and that): y = 'hello ''world'#FIXME: https://github.com/psf/black/issues/26
class Foo ( object ):
def f (self ):
return 37*-2
def g(self, x,y=42):
return y
def f ( a: List[ int ]) :
return 37-a[42-u : y**3]
def very_important_function(template: str,*variables,file: os.PathLike,debug:bool=False,):
"""Applies `variables` to the `template` and writes to `file`."""
with open(file, "w") as f:
...
# fmt: off
custom_formatting = [
0, 1, 2,
3, 4, 5,
6, 7, 8,
]
# fmt: on
regular_formatting = [
0, 1, 2,
3, 4, 5,
6, 7, 8,
]
Running ruff format
, reformats Python code using Black's style guidelines. You'll notice a lot of spacing problems were automatically fixed. There is a way to ignore sections by wrapping code blocks between # fmt: off
and # fmt: on
. That's why the custom_formatting
list retains its original format.
"""
Reformatted Python Code
"""
from seven_dwwarfs import Grumpy, Happy, Sleepy, Bashful, Sneezy, Dopey, Doc
x = {"a": 37, "b": 42, "c": 927}
x = 123456789.123456789e123456789
if (
very_long_variable_name is not None
and very_long_variable_name.field > 0
or very_long_variable_name.is_debug
):
z = "hello " + "world"
else:
world = "world"
a = "hello {}".format(world)
f = rf"hello {world}"
if this and that:
y = "hello world" # FIXME: https://github.com/psf/black/issues/26
class Foo(object):
def f(self):
return 37 * -2
def g(self, x, y=42):
return y
def f(a: List[int]):
return 37 - a[42 - u : y**3]
def very_important_function(
template: str,
*variables,
file: os.PathLike,
debug: bool = False,
):
"""Applies `variables` to the `template` and writes to `file`."""
with open(file, "w") as f:
...
# fmt: off
custom_formatting = [
0, 1, 2,
3, 4, 5,
6, 7, 8,
]
# fmt: on
regular_formatting = [
0,
1,
2,
3,
4,
5,
6,
7,
8,
]
Default Configurations and Change Rule Selection
Using ruff with its default configurations is a good starting point if this is your first time using a linter and formatter.
Ruff can be configured using the pyproject.toml
file. The default configurations can be found in the documentation here. I recommend changing the configuration once you're comfortable using ruff or already have experience with other linters and formatters.
One useful configuration is adding isort for organizing imports to the linting rules. Simply add "I" to the select
list under the [tool.ruff.lint]
section. The full list of rules can be found in the Rules section of the documentation and where "I" stands for isort.
# Copy pasted the default configuration and added "I" to the `select` list.
[tool.ruff.lint]
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
# McCabe complexity (`C901`) by default.
select = ["E4", "E7", "E9", "F", "I"]
ignore = []
Integrations
There are a couple of different ways of integrating ruff into your workflow. Instead of running ruff every single time you change a file, there are ways run the tool automatically. I recommend using a pre-commit hook as it checks the file before committing.
If the command line isn't for you, VS Code has a ruff extension in the marketplace.
GitHub Actions can run ruff on git push to a specific branch.
pre-commit hook can run ruff when git commit is done locally.
Conclusion
Ruff is a powerful tool to use as all the Python linting and formatting tools have become consolidated into a single, fast, and efficient solution. Whether you're just starting to use a linter and formatter, or a seasoned developer, using a linter and formatter is a must for writing clean and maintainable code.
Subscribe to my newsletter
Read articles from Chris Sato directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
