Python: lint code faster
Ruff: your new best friend for fast code linting
The default linter many Pythonista know and use is probably Flake8. I’m also a huge user of this excellent library. But recently, I discovered Ruff, and it seems to be a game changer. I will introduce some of its features in this article.
There are two ways to install Ruff, python style or globally. You will need python3.7 or higher installed with your package manager installed like pip, conda, or poetry for the python style.
$ pip install ruff # or $ poetry add ruff -G dev # or $ conda install -c conda-forge ruff
You can also install it globally using tools like pipx, brew, or your default OS package manager in some cases.
$ pipx install ruff # or for Mac OS $ brew install ruff # or for Arch Linux $ pacman -S ruff # or for Alpine $ apk add ruff
Let’s consider the following snippet of code:
from typing import List import os def sum_even_numbers(numbers: List[int]) -> int: """Given a list of integers, return the sum of all even numbers in the list.""" return sum(num for num in numbers if num % 2 == 0)
To check this code with Ruff, you will run:
$ ruff check . numbers/numbers.py:3:8: F401 [*] `os` imported but unused Found 1 error. [*] 1 potentially fixable with the --fix option.
The dot at the end means the current working directory, so it will scan all python files in this directory and sub-directories. You generally will run this command in the project root directory. Ruff will show you all the errors it encountered when parsing the source code. Note that it uses the same error codes as Flake8 so you will not be disoriented. 😉
If you want to fix the code like autoflake does, simply add a fix option to your Ruff command:
$ ruff check --fix .
Now the code will look like this:
from typing import List def sum_even_numbers(numbers: List[int]) -> int: """Given a list of integers, return the sum of all even numbers in the list.""" return sum(num for num in numbers if num % 2 == 0)
It looks good. Here are other ways to invoke Ruff:
# Lint all files in `/path/to/code` (and any subdirectories) $ ruff check path/to/code/ # Lint all `.py` files in `/path/to/code` $ ruff check path/to/code/*.py # Lint `file.py` $ ruff check path/to/code/to/file.py
You can also run Ruff in watch mode to automatically re-run when files changed.
$ ruff check path/to/code/ --watch
Note that Ruff will ignore all files and directories present in a .gitignore file if you use Git as your code source version control system.
To configure Ruff, you will use a ruff.toml file or a pyproject.toml file. All configurations in this file will be under the section
[tool.ruff]. By default Ruff supports pyflakes and pycodestyle rules (the basis of Flake8) but you can configure more rules from more than 40 plugins implemented like pyupgrade and isort (yes you can even sort imports with Ruff!).
[tool.ruff] line-length = 100 # defaults to 88 like black target-version = "py39" # the python version to target, useful when considering code upgrades, defaults to "py310" select = [ "E", # pycodestyle "F", # pyflakes "UP", # pyupgrade, "I", # isort ] # if you want to configure a particular plugin, you can do it in # a subsection, it is usually the same configuration that the plugin # supports [tool.ruff.isort] ...
Now if you have python 3.9 or higher installed and you re-run Ruff with this configuration, you will see the following output:
$ ruff check . numbers/numbers.py:5:31: UP006 [*] Use `list` instead of `List` for type annotations Found 1 error. [*] 1 potentially fixable with the --fix option.
You can fix the code with the
—fix option like before and it will look like this:
def sum_even_numbers(numbers: list[int]) -> int: """Given a list of integers, return the sum of all even numbers in the list.""" return sum(num for num in numbers if num % 2 == 0)
Also, note that Ruff uses a hierarchical configuration, such as the closest pyproject.toml file in the directory hierarchy is used to check python files. Note that configurations are not merged with pyproject.toml files found in parent directories. If you want to re-use some configuration, you should explicitly tell the current file where to find additional configuration.
# Extend the `pyproject.toml` file in the parent directory. extend = "../pyproject.toml" # But use a different line length. line-length = 100
To know more about configuration, don’t hesitate to look at this section of the documentation when needed.
Directly in code
You can ignore errors on a line level.
from typing import List def sum_even_numbers(numbers: List[int]) -> int: # noqa: UP006 """Given a list of integers, return the sum of all even numbers in the list.""" return sum(num for num in numbers if num % 2 == 0)
Or on the entire file. The
noqa statement must be at the top of the file.
# ruff: noqa: UP006 from typing import List def sum_even_numbers(numbers: List[int]) -> int: """Given a list of integers, return the sum of all even numbers in the list.""" return sum(num for num in numbers if num % 2 == 0)
In the configuration file
# Enable flake8-bugbear (`B`) rules. select = ["E", "F", "B"] # Never enforce `E501` (line length violations). ignore = ["E501"] # Avoid trying to fix flake8-bugbear (`B`) violations. unfixable = ["B"] # Ignore `E402` (import violations) in all `__init__.py` files, and in `path/to/file.py`. [per-file-ignores] "__init__.py" = ["E402"] "path/to/file.py" = ["E402"]
We can use the
per-file-ignores options for that purpose. I also configure the unfixable option for the bugbear plugin which means that whenever I call
ruff check . —fix, Ruff will never try to fix errors reported by this plugin.
On the command line
$ ruff check path/to/code/ --ignore E402
Continuous integration / Continuous deployment
To help with your workflow, Ruff comes with a pre-commit hook you can use.
- repo: https://github.com/charliermarsh/ruff-pre-commit # Ruff version. rev: 'v0.0.257' hooks: - id: ruff
If you want to enable auto-fix, you can complete the previous configuration like this:
- repo: https://github.com/charliermarsh/ruff-pre-commit # Ruff version. rev: 'v0.0.258' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix]
It is worth noting that you can format how the output will appear in your favorite CI/CD platform with the format configuration option.
[tool.ruff] format = "gitlab"
While it is still in beta, it is already used by well-known projects like FastAPI, Pandas, and Bokeh. I’m not really scared about its evolution. It is already a perfect replacement for Flake8 in my opinion. If you want to know what features might be added this year (2023) to this tool, I recommend that you read this blog post by the author itself. He thinks about an autoformatting tool for example to replace Black. Anyways, I’m really excited to see what will come this year!
This is all for this article, hope you enjoy reading it. If you want to know more about Ruff, I can only refer you to the official documentation.
Take care of yourself and see you soon! 😁
Woudar's Blog is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.