Boosting Python development speed with Ruff: An all-in-one lightning fast linter

Discover how you can 10x to 100x improve your linter and pre-commit checks by adding Ruff module and rules to your project.

Ruff Python library alternative logo.

The Need for Speed. Discover the Ruff module, Ruff rules and the migration from Black

In the world of software development, every second counts. For Python developers, the time spent on code linting can sometimes be a real productivity killer. But what if there was a tool that could make this process lightning-fast, giving you nearly real-time feedback as you code? Meet Ruff, the game-changing Python linter that’s about to revolutionize your development workflow.

Python is renowned for its simplicity and readability, making it a go-to language for many developers. However, as projects grow in complexity, the build times and code analysis can slow down significantly. This is where Ruff steps in. It’s based on a simple but powerful idea: Python tooling could be much, much faster.

Imagine a Python linter that’s approximately 150 times faster than Flake8 on macOS. That’s Ruff for you. It’s not just slightly faster; it’s blazingly fast. For instance, when linting the entire CPython codebase from scratch, it’s the difference between sitting around for 12+ seconds and getting almost instant feedback in just 300-500 milliseconds. It’s a game-changer that saves you time and makes your development process more efficient.


How Ruff Works

Ruff is written in Rust, but here’s the magic: as a user, you won’t even notice it’s not written in Python. It leverages Rust Python’s AST parser and implements its own AST traversal, visitor abstraction, and lint-rule logic. It supports Python 3.10 and 3.11, including the new pattern matching syntax. Ruff is also pip-installable, making it easy to incorporate into your workflow.

Ruff is built on two core hypotheses: Python tooling could be rewritten in more performant languages, and an integrated toolchain can tap into efficiencies that aren’t available to a disparate set of tools. Ruff exemplifies these ideas by generating all violations in a single pass and even autofixing issues without a noticeable performance penalty.

If you’re curious to experience the speed and efficiency of Ruff for yourself, you can try it today by running or the package manager you are using, like Poetry poetry add ruff.

pip install ruff

As linter, as it is stated in the Ruff documentation, ” designed as a drop-in replacement for Flake8 (plus dozens of plugins), isort, pydocstyle, pyupgrade, autoflake, and more.” Ruff has a set of over 700 rules in its linter functionallity.

You can start by running it for all current folder files:

ruff check .

If you want to run the Ruff formatter for the first time, as a starting step I would recommend you to use ruff format . --diff. The --diffflag allows you to review the modifications before they are applied. It is thought as a drop-in replacement for Black.


Integrating Ruff in you project with Pyproject yaml and pre-commit

Till today, there is not a mantained way to migrate from Black to Ruff) as it would be a one-timer and costly to ongoing support.

poetry add ruff

With the following code you will see

ruff check .
# or ruff . 

Suposse the folowing pyproject.toml file that could be usual in a data related project:

[tool.poetry]
name = "data-project"
version = "1.0.0"
description = "Python 🐍 service that is intended to do wonderful things✧"
authors = ["TypeThePipe.com"]
readme = "README.md"
classifiers = [
    "Topic :: Software Development :: Libraries :: Python Modules",
    "Programming Language :: Python :: 3.11",
]


[tool.poetry.dependencies]
python = "~3.11.0"
sqlmodel = "^0.0.8"
pydantic = "^1.10.4"
numpy = "^1.24.2"
pandas = "^1.5.3"
scipy = "^1.10.1"
statsmodels = "^0.13.5"
rollbar = "^0.16.3"
sqlalchemy = "1.4.35"
alembic = "^1.10.2"
psycopg2 = "^2.9.5"


[tool.poetry.group.dev.dependencies]
black = "^23.1"
flake8 = "^4.0"
isort = "^5.10"
flakeheaven = "^0.11"
pytest = "^7.2"
pytest-postgresql = "^4.1.1"
psycopg = "^3.1.8"
coverage = {version = "^7.2.1", extras = ["toml"]}
pre-commit = "^2.17.0"
mypy = "^1.0.1"
pytest-dependency = "^0.5.1"
dvc = { version = "^2.10.2", extras = ["s3"] }
mkdocs = "^1.4.2"
mkdocstrings = "^0.19.1"
mkdocs-material = "^9.0.5"


[tool.poetry.group.notebook.dependencies]
jupyterlab = "^3.6.1"
notebook = "^6.5.2"
plotly = "^5.13.0"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"


#####
# Typing
#####

[tool.isort]
# Black Compatibility
profile = "black"

[tool.mypy]
python_version = "3.11"
warn_return_any = true
warn_unused_configs = true
ignore_missing_imports = true
exclude = ["nb"]

#####
# Flake8
#####

[tool.flakeheaven]
exclude = ["README.md"]
format = "stat"
max_line_length = 120 # YOLO
show_source = true

[tool.flakeheaven.plugins]
pyflakes = ["+*", "-F401"]
# enable only codes from S100 to S199
flake8-bandit = ["-*", "+S1??"]
flake8-docstrings = ["+*"]
flake8-bugbear = ["+*"]
flake8-builtins = ["+*", "-A003"]
flake8-comprehensions = ["+*"]
flake8-eradicate = ["+*"]
flake8-isort = ["+*"]
flake8-mutable = ["+*"]
flake8-pytest-style = ["+*"]
pyflakes = ["+*"]
pylint = ["+*"]

Then you could change all the linter as a replacement for Flake8, isort, autoflake and pyupgrade.

This Ruff code would be the replacement of the Flake8 pyproject section. We use the tool.rufffor generic configuration and the tool.ruff.lint for specific Ruff rules you want to apply in your linter. Those selected ones would be the replacement of the previous configuration.

[tool.ruff]
line-length = 120 # YOLO
target-version = "py311"

[tool.ruff.lint]
select = [
    "F", #Pyflakes
    "B", #flake8-bugbear
    "A", #flake8-builtins 
    "C4", #flake8-comprehensions
    "ERA", #eradicate
    "I", #isort
    "PT", #flake8-pytest-style
    "C90", #mccabe
    "N", #pep8-naming
    "E", #pycodestyle error
    "PL" #Pylint
]
ignore = ["A003"]

[tool.ruff.format]
quote-style = "single"
indent-style = "tab"

You have to take into account that the Bandit Ruff port is still an ongoing track of work, so I’ve not included it and let it as an extra step of my pre-commit file. You can keep update about this integration in this Github Issue.

Also you can find the pre-commit file useful, as it is an incredible and necessary tool while building robust code projects.

#.pre-commit-config.yaml file
- repo: https://github.com/astral-sh/ruff-pre-commit
  # Ruff version.
  rev: v0.1.6
  hooks:
    # Run the linter.
    - id: ruff
      args: [ --fix ]
    # Run the formatter.
    - id: ruff-format
- repo: https://github.com/tox-dev/pyproject-fmt
  rev: "1.5.2"
  hooks:
    - id: pyproject-fmt

What is the difference between using tool.lint and tool.ruff.lint?

The tool.ruff.lint is now preferred the way to configure the linter, despite it is sill experimental and may include changes.


Stay updated on Ruff and Python tips

Hopefully, this post has helped you become familiar with a newcomer to the Python linter and pre-commit hook landscape, say hi to Ruff.

If you want to stay updated…

Carlos Vecina
Carlos Vecina
Senior Data Scientist at Jobandtalent

Senior Data Scientist at Jobandtalent | AI & Data Science for Business

Related