Five months ago, when I released Ruff, I wasn’t sure whether people would care about a faster Python linter. I was blown away by the response (HN, Twitter). That enthusiasm gave me the energy, motivation, and space to fully commit myself to the project.
I built Ruff based on some of my own frustrations with Python tooling. I felt there was an opportunity to leverage Rust to build faster, better tools — the tools I wanted! But even I underestimated how impactful a “faster Python linter” could be, and how swiftly the community might adopt it.
Today, Ruff is used as the primary linter in some of the most popular and significant Python projects on GitHub, including Pandas, FastAPI, Apache Airflow, and more.
If you’d told me, five months ago, that any one of these projects was even considering using Ruff, I wouldn’t have believed you. To see them all migrate to Ruff, among so many others that I admire — despite being pre-1.0, despite the docs being one long README — has made me extremely bullish on the project and the ideas behind it.
I meant to write this post after the first 100 releases, but now (v0.0.226) seems as good a time as any to recap the progress we’ve made over the last few months, and share more on where we’re headed.
Functionality
Ruff was released a little too early (it was “rough”) — but, that was intentional. My goal was to ship a proof-of-concept, and see if the ideas struck home. We launched with something like 20 lint rules.
Today, Ruff supports 376 lint rules, including all of Pyflakes and Pycodestyle (apart from those rules made redundant by Black), all of flake8-bugbear, flake8-comprehensions, flake8-simplify, flake8-annotations, flake8-docstrings, flake8-comprehensions, flake8-eradicate, pep8-naming, most of pyupgrade, and more, giving Ruff near-complete coverage across the most popular Flake8 plugins.
Ruff can also be used as a drop-in replacement for isort.
We’ve reimplemented each of these rules from scratch, in Rust.
In many cases, Ruff can automatically fix the relevant violations. For example, Ruff can automatically fix almost all of the flake8-comprehensions rules, if you have them enabled:
Like pyupgrade, Ruff can automatically upgrade your type annotations based on the current Python version:
Despite the increased scope, Ruff has gotten… faster?
And these dramatic speed-ups have been validated in the wild:
- Dagster reported a ~1000x speed-up over Pylint.
- Bokeh reported a ~150-200x speed-up over Flake8.
- Airflow reduced their pre-commit time from 1m40s to 6s.
- Pandas runs Ruff in 3.35s. pyupgrade alone takes 21.74s on that codebase.
The velocity of the project, too, remains very high. We release at least once a day (”I want Ruff to feel like a constant stream of improvements and power-ups for your workflow”). Every release contains something significant. Despite this cadence, we’ve shipped ~five breaking changes over the life of the project.
Beyond this “core” functionality, Ruff and the ecosystem around it have matured in a few significant ways — here are some highlights:
- We released an official VS Code extension, which supports Quick Fix actions to autofix lint violations, sort imports, and more.
- We released an official Language Server Protocol (LSP) implementation, making Ruff accessible to any LSP-supporting editor (Neovim, Sublime Text, Emacs, etc.).
- We released the Ruff Playground, an in-browser demo powered by a WASM build of Ruff. I’ll keep this one brief as it deserves its own blog post — but it’s extremely cool…
- We released flake8-to-ruff, a tool to automate the migration from Flake8 to Ruff.
- We upgraded our configuration model to be monorepo friendly, supporting the kind of hierarchical configuration that you see in tools like ESLint. (Dagster runs Ruff over 50+ Python modules in a single invocation.)
- We released Conda and Homebrew distributions, giving you more ways to install Ruff.
Adoption
Today, Ruff is used as the primary linter in some of the most popular and significant Python projects on GitHub, including Pandas, FastAPI, Apache Airflow, and more.
Probably the most complex and rewarding deployment (at least for me) thus far came from Dagster, where they replaced 70 parallel CI jobs (to run Pylint) with a single pre-commit hook. It runs in 400 milliseconds, a ~1000x speed-up over their previous setup.
It’s a perfect encapsulation of the impact that “faster tools” can have on a project, on a team, on a company. Ruff isn’t just faster; it’s so much faster that it completely changes the workflow.
There are a bunch of other projects and companies using Ruff — I’d love to mention each of them by name, but beyond Pandas, FastAPI, and Airflow, here are a few that’ve helped evangelize and push Ruff forward:
- Zulip
- Jupyter
- Pydantic
- Polars
- Bokeh
- Hatch
- OpenBB (via OpenBB Terminal)
- Snowflake (via the SnowCLI)
- Matrix (via Synapse)
Beyond these ‘high-profile’ projects, though, I’m genuinely grateful to anyone that’s willing to give Ruff a try, and especially to those that’ve stuck with it. Adopting a new tool is a big decision! Thank you for your trust. I won’t let you down.
Community
The numbers don’t do it justice, but to get them out of the way, Ruff is up to…
- 79 Contributors
- 1,231 Pull Requests
- 656 Closed Issues (106 Open)
- 226 Releases
(On that note: if you’re interested in contributing, we’d love to have you involved. I’m biased, but I consider Ruff to be a very contributor-friendly project, and a good choice for those in the Python ecosystem with an interest in learning Rust.)
We’ve also established a great working relationship with the RustPython project, contributing back a bunch of compatibility improvements to their parser. I’m grateful to the RustPython team for their support and willingness to help push Ruff forward.
Though it can't be quantified, a major highlight for me has been the amount of optimism and goodwill that I see in every community interaction. Almost every Issue includes some message of gratitude or excitement.
This matters a lot to me. I want everyone who stops by the repo to have a good experience, and to feel respected — even if their issue is marked as wontfix, or they file a duplicate, or their feature gets deprioritized. To me, that’s a critical part of developer experience. And while it may sound a little melodramatic, I view those interactions and those relationships as part of Ruff itself.
Maintaining this kind of energy is an intentional goal of mine, and something that I put a lot of effort into. It gives a lot back in return — hearing from users and solving their problems is a huge source of motivation. I view every interaction as an opportunity to build more excitement around and support for Ruff.
Looking Forward
The natural next step for Ruff is to extend it into a fully-fledged autoformatter. And that’s where I’ll be focused for now. A natural next next step would be a type checker, but… one thing at a time.
I’m also thinking hard about what a “stable” release will look like, and how Ruff’s API and abstractions will evolve as its viewed less as a reimplementation of existing tools and more as a standalone tool in its own right, with its own curated rule set.
My vision, though, goes beyond linting and static analysis. If Ruff proves anything, it’s that there’s appetite for new tooling in the Python ecosystem — that “if you build [something pretty good], they will come.”
I’ve taken that lesson to heart. My goal is to bring more Ruff-like experiences to Python — to take the ideas and attitudes behind Ruff, and apply them to more problem domains. Let’s see where that takes us! (“If your developer tools were instant, what would that unlock?”)
P.S. If you’re already using Ruff, or just interested in sharing the challenges you face with Python tooling (linters, type checkers, virtual environments, package management, etc.), I’d love to hear from you. (You can also DM me on Twitter.)
Published on January 19, 2023.