TL;DR: Ruff combines different tools like linters, security analyzers, and formatters into one unique simple fast tool with a standard configuration. Pre-commit streamlines git pre-commit hooks by standardizing configurations and offering community-made hooks. Together, they greatly ease Python development, both individually and in a team setting.
I’m coding in Python since 2006 (17 years !) and I have changed my tooling several times along with the ecosystem evolution.
My previous IDE setup was Vim-oriented with the additional plugins SimpylFold, YouCompleteMe, syntastic, nerdtree, ctrlp.vim among others.
I’ve moved since 5 years to VSCode as a primary editor due to the capacity to be integrated with a lot of use cases in an easier way :
And so on.
Microsoft recently (September Announcement) moved the tools support offered in the Python extension for Visual Studio Code into separate extensions. This stems me to review the alternative I had in mind instead of directly re-installing my current tooling.
These tools — Pylint, Flake8, Black Formatter, Bandit, and isort — help enhance my Python code quality by offering linting, error detection, type checking, automated formatting, security analysis and import sorting capabilities.
It was the occasion to (re)test the latest kid on the block: Ruff
Ruff is an extremely fast open-source Python linter and code formatter, written in Rust
It has rapidly become popular for its swift performance and its ability to aggregate the features of multiple linters and is now used in large projects and organisations as the tool of choice.
The README on Github list its principal qualities:
pip
pyproject.toml
supportI already tried Ruff two years ago but there was still some issues (notably not all pylint and bandit checks were there).It is now quite mature even if not full feature-parity with Pylint. However it does 80% of the job in 2% of the previous time (yes I voluntarily broke Pareto). To sum it up, Ruff is now my tooling of choice because:
All-in-one: Replace dozens of tools with a single, unified interface. Ruff supports over 700 rules unifying my previous tools (Pylint, Flake8, Black Formatter, Bandit,) and maintains drop-in compatibility.
Fast: It is not fast but blazing fast. Check this comparison below.
Automated and easy configuration: Automatically upgrade to newer Python syntax, organize imports, remove unused variables and specify all your validation logic in one configuration file.
One of the nice aspect of Ruff is its configuration integration in the pyproject.toml format.
The pyproject.toml
file is a configuration file format introduced in PEP 518 for Python projects. It is designed to consolidate various settings and configurations that were traditionally spread across multiple files (setup.py
, setup.cfg
, requirements.txt
, etc.) into a single, standardized file. Here’s an overview of the standard sections and elements you might find in a typical pyproject.toml
file:
[build-system]
: This section specifies the build system requirements, particularly useful for packaging and distribution. It usually includes:
requires
: A list of packages required to build the project.build-backend
: The build backend, often setuptools.build_meta
.[tool]
Sections: These sections are used by various Python tools for their configurations. Each tool has its own subsection.
[project]
(optional in PEP 621): This newer section is for metadata about the project itself, such as:
name
, version
, description
: Basic project information.dependencies
: A list of runtime dependencies.optional-dependencies
: Dependencies needed for optional features.authors
, maintainers
: Information about the people involved in the project.Custom Configurations: Besides these standard sections, pyproject.toml
can also include custom configurations for other tools that support it. This flexibility allows it to cater to the specific needs of a project.
Advantages:
In practice, the contents of pyproject.toml
can vary greatly depending on the tools used and the specific needs of the project. Its adoption has been growing, as it represents a more modern and unified approach to managing Python project configurations.
Ruff is available as ruff
on PyPI:
pip install ruff
Once installed, you can run Ruff from the command line:
ruff check . # Lint all files in the current directory.
ruff format . # Format all files in the current directory.
For macOS Homebrew and Linuxbrew users, Ruff is also available as ruff
on Homebrew:
brew install ruff
Here an example on how the check looks:
❯ ruff check .
business-facing/main.py:1:1: I001 [*] Import block is un-sorted or un-formatted
business-facing/main.py:325:9: B007 Loop control variable `region` not used within loop body
business-facing/main.py:340:17: B018 Found useless expression. Either assign it to a variable or remove it.
business-facing/main.py:531:8: SIM118 Use `key in dict` instead of `key in dict.keys()`
Found 4 errors.
[*] 1 fixable with the `--fix` option (2 hidden fixes can be enabled with the `--unsafe-fixes` option).
You can also install the Ruff VScode extension to do it live when coding (integration as Vscode linter/formatter).
I then personnaly set it up to format on save and define it as default formatter by editing my Json preference in the Settings editor like this:
{
"[python]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "charliermarsh.ruff"
}
}
The Settings editor is the UI that lets you review and modify setting values that are stored in a settings.json
file. You can review and edit this file directly.
File -> Preferences -> Settings -> Extensions -> Scroll down
and find "Edit in settings.json"
from the Command Palette (Ctrl+Shift+P on Windows, ⇧⌘P on Mac) with Preferences: Open User Settings (JSON)
In these paths in your OS
Windows %APPDATA%\Code\User\settings.json
macOS $HOME/Library/Application Support/Code/User/settings.json
Linux $HOME/.config/Code/User/settings.json
Here an example on how it looks with deliberates mistakes:
And after saving and having ruff format and auto fix (os import fix, trailing whitespaces, blank lines, etc):
And finally after fixing the mistakes remaining manually:
A Git hook is a script that Git executes before or after events such as commit, push, and receive. These hooks are customizable and allow you to automate certain tasks in the Git workflow. They are stored in the .git/hooks
directory of a Git repository and can be written in any scripting language.
A pre-commit hook is a specific type of Git hook that runs before a commit is finalized. This hook is used to inspect the snapshot that’s about to be committed. For example, it can check for code formatting, lint errors, or run tests. If the pre-commit hook script exits with a non-zero status, the commit is aborted, allowing you to fix any issues before proceeding.
The pre-commit framework ensures a consistent coding style and standards across the team. By using shared pre-commit configurations, all team members automatically run the same checks before committing their code. This consistency helps in maintaining code quality and reduces the time spent on reviewing code for style compliance.
Pre-commit has a strong community that contributes to a shared repository of pre-commit hooks. These community-made hooks cover a wide range of languages and use cases and provides a well-maintained, regularly updated tools. Hooks in the pre-commit framework are easily reusable across multiple projects. . This reusability saves time and effort in setting up project environments and ensures that new projects adhere to established standards right from the start.
To set up Pre-commit in a Python project, follow these simple steps:
Install Pre-commit: Add Pre-commit as a development dependency by running pip install pre-commit
Create a Pre-commit Configuration File: Make a .pre-commit-config.yaml
file in your project’s root folder. This is where you’ll list the hooks you want to use.
Define Hooks: In your configuration file, specify the hooks for tasks like code formatting or linting. For example, to use a simple fix for the trailing whitespace, here is the configuration.
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
Install Git Hooks: Run pre-commit install
to set up the hooks. Pre-Commit Framework will fetch the hooks defined and will cache them locally as well link itself in the git pre-commit hook. pre-commit by default places its repository store in ~/.cache/pre-commit
.
Commit Changes: When you make a commit, Git will run the pre-commit hook which will hence run the previously installed hooks.
Manual Testing: To manually run the hooks on staged files, use pre-commit run
. If you want to run of the whole repository (so files not staged right now), use pre-commit run -a
.
Handling Issues: If Pre-commit finds problems, it’ll stop the commit. Fix the issues and try committing again.
Updating hooks automatically ¶ :You can update your hooks to the latest version automatically by running pre-commit autoupdate
. By default, this will bring the hooks to the latest tag on the default branch.
pre-commit extension in the Marketplace helps to run pre-commit directly into VSCode.
In summary, the pre-commit framework offers a robust solution for automating and standardizing code quality checks. Its integration into the Git workflow, combined with the support of a strong community and the ease of reusing hooks across projects, makes it an invaluable tool for maintaining high-quality code in collaborative environments.
Now that we have explained how both tools are working, here is below the configuration to make them work seamlessly together.
[tool.ruff]
# Exclude common directories that are typically not part of the source code or are generated by tools.
exclude = [
".bzr",
".direnv",
".eggs",
".git",
".git-rewrite",
".hg",
".mypy_cache",
".nox",
".pants.d",
".pytype",
".ruff_cache",
".svn",
".tox",
".venv",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"venv",
"business-facing/layer", #OKAPI Layer
]
# Set the maximum line length to 127 characters.
line-length = 127
# Define the number of spaces used for indentation, aligning with Black's style.
indent-width = 4
# The minimum Python version to target, e.g., when considering automatic code upgrades,
# like rewriting type annotations
target-version = "py38"
[tool.ruff.lint]
# Enable Pyflakes (F) and a subset of the pycodestyle (E) codes by default.
# pycodestyle warnings (W)
# Activate Security Rules (S) to replace bandit
# Enable the isort rules (I) to replace isort
# flake8-bugbear (B)
# flake8-simplify (SIM)
select = ["F", "E4", "E7", "E9", "W", "S", "I", "B","SIM"]
ignore = [] # List any rules to be ignored, currently empty.
# Allow auto-fixing of all enabled rules when using the `--fix` option.
fixable = ["ALL"]
unfixable = [] # Specify rules that cannot be auto-fixed, if any.
# Define a regex pattern for allowed unused variables (typically underscore-prefixed).
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
[tool.ruff.format]
# Enforce double quotes for strings, following Black's style.
quote-style = "double"
# Use spaces for indentation, in line with Black's formatting style.
indent-style = "space"
# Keep magic trailing commas, a feature of Black's formatting.
skip-magic-trailing-comma = false
# Automatically detect and use the appropriate line ending style.
line-ending = "auto"
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: end-of-file-fixer
exclude: "business-facing/layer"
- id: trailing-whitespace
exclude: "business-facing/layer"
- id: check-yaml
exclude: "business-facing/layer"
- id: check-json
exclude: "business-facing/layer"
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.1.6
hooks:
# Run the linter.
- id: ruff
# Run the formatter.
- id: ruff-format
Here is a concrete usage :
It doesn’t add more time to your workflow but add a local check capacity quite smoothly.
As you have seen, there is several advantages to this combination.
Standardizing Code: Using Ruff with pre-commit hooks makes everyone’s code look the same. This is great for keeping the code neat and easy to understand.
Ready for CI/CD Pipelines: It also helps with setting up CI/CD pipelines (automatic testing and deployment) because the code is already clean and follows the rules hence will pass the same test done in the pipeline.
Fast and Powerful: Ruff is quick and does a thorough job of checking the code without slowing down coding. Adding pre-commit ensure that nothing is forgotten before pushing.
Better Code Quality: With Ruff, code quality improves because it catches errors early. This leads to fewer bugs and problems later on.
Easy to Use: Ruff is user-friendly. It works well for both new and experienced programmers, making it easier for everyone to contribute good quality code.
Improves Teamwork: When everyone’s code follows the same standards, working together is easier. It reduces confusion and makes combining code from different people smoother.
Saves Time and Effort: Finally, Ruff in pre-commit hooks saves time. It automatically checks the code, so developers spend less time fixing style issues and more on creating new features.
In summary, using Ruff with pre-commit hooks makes coding and teamwork smoother, ensures high-quality code, and saves time. This approach is really helpful for teams and makes managing code a lot easier.
PS: I didn’t explore in this article cookiecutter and poetry that complement perfectly Ruff and pre-commit.