Hi, I'm Matthias

I am a founding partner of Feinheit AG and Die Bruchpiloten AG. Find me on GitHub, Mastodon, LinkedIn or by email.


How ruff changed my Python programming habits

ruff isn’t just a faster replacement for flake8, isort and friends.

With other Python-based formatters and linters there’s always a trade off between development speed (waiting on git commit is very boring) and strictness.

ruff is so fast that enabling additional rules is practically free in terms of speed; the only question is if those rules lead to better, or maybe just to more correct and consistent code.

I have long been using pre-commit, and even longer flake8, black, isort. I have written a piece about flake8 and the value of standards almost 9 years ago and have continued moving in the mentioned direction ever since.

These days I have enabled a wide variety of rules. I’m not sold on all of them (looking at you, pylint) and I’m definitely not of the opinion that rules which I’m not using currently are worthless. I didn’t even know most of these rules before starting to use ruff, and ruff making them easy and painless to use (without a measureable performance penalty) has certainly lead to me annoying my coworkers with a growing set of enabled rules.


The current ruleset and some justifications for it follows.

pyflakes, pycodestyle

No justification necessary, really.


I like the cyclomatic complexity checker, but I have relaxed it a bit. I find it very useful to avoid complex code, but some code is totally straightforward (e.g. building a queryset from a wide variety of query parameters) but still has many if statements. I’d rather allow more complexity instead of sprinkling the code with # noqa statements.


Sorted imports are great.


Mostly great except when it flags Django’s migration files. The filenames always start with numbers and that’s obviously not a valid Python module name, but it’s not supposed to be.


pyupgrade is totally awesome.


Avoiding non future-proof uses of sys.version and sys.version_info is a good idea, no questions about that.


Sometimes annoying, mostly useful. I don’t like that the plugin flags e.g. with_tree_fields(True) or with_tree_fields(False) because I don’t think this could be possibly misread. But, apart from these edge cases it really is a good idea, especially since keyword-only arguments exist and those aren’t flagged by this rule.


Mostly useful. I have disabled the zip() without strict= warning.


Checks for unnecessary conversions between generators and lists, sets, tuples or dicts.


I actually like consistency. I also like flagging fields = "__all__", but this check shouldn’t trigger in admin ModelForm classes, really. I probably have to add another entry to [tool.ruff.per-file-ignores] for this.


Quite a random assortment of rules. I like the no-unnecessary-pass and no-pointless-statements rules, among others.


Nice simplifications. I’m not sure if ternary opeartors are always a plus, especially since they hide the branching from coverage.


Enormously useful and important. I don’t know how many times I have encountered broken code like gettext("Hello {name}".format(name=name)) instead of gettext("Hello {name}").format(name=name).


Avoids eval(). Avoids blanket # noqa rules (always be specific!)


I have been using all pylint rules for some time. The pylint refactoring rules (PLR) did prove to be very annoying so I have reverted to only enabling errors and warnings.

The two main offenders were PLR0913 (too many arguments) and PLR2004 (magic value comparison). The former would be fine if it would count keyword-only arguments differently; it’s certainly a good idea to avoid too many positional parameters, I don’t think keyword-only parameters are that bad. The latter is bad because often the magic value is really obvious. If you’re writing code for the web you shouldn’t have to use constants for the 200 or 404 status codes; one can assume that they are well known.


Ruff is able to automatically remove # noqa statements which don’t actually silence any warnings. That’s a great feature to have.

Line length

Yes, let’s go there. I still don’t use longer lines than +/- 80 characters, but I have disabled all line length warnings these days. I don’t want to be warned because I didn’t break a string across lines.

Rules I don’t like

  • flake8-builtins: Too many boring warnings. I didn’t even want to know that copyright is a Python builtin.
  • flake8-logging-format: Not generally helpful. Avoiding different exception strings so that e.g. Sentry can group exceptions more easily is a good idea, but the rule generated so many false positives as to be not useful anymore.

Final words (for now)

I really hope that Black is integrated into ruff one day.

Also, I hope that ESLint and prettier will be replaced by a faster tool. I have my eyes on a few alternatives, but they are not there yet for my use cases.