cat posts/ruff-linter-und-formatter.md
Ruff: Linter und Formatter in einem
Ruff findet Fehler, modernisiert Python-Code, sortiert Imports und übernimmt die Formatierung – schnell, zentral konfiguriert und direkt in Editor, pre-commit und CI integrierbar.
In Teil 6 der Python-Serie ging es darum, Code möglichst klar und pythonic zu schreiben.
Im Alltag musst Du aber nicht jede Stilregel selbst im Kopf behalten. Dafür gibt es zwei unterschiedliche Werkzeuge:
- Ein Linter analysiert den Code und weist auf Fehler, verdächtige Stellen oder unerwünschte Schreibweisen hin.
- Ein Formatter bringt den Code automatisch in eine einheitliche Form.
Lange Zeit bestand ein typisches Setup deshalb aus mehreren Tools: Flake8 oder Pyflakes fürs Linting, Black für die Formatierung, isort für Imports, pyupgrade für modernere Syntax und dazu noch diverse Flake8-Plugins.
Ruff deckt einen großen Teil davon mit einem
einzigen Werkzeug ab. Es ist in Rust geschrieben, bringt Linter, Formatter und
Import-Sortierung mit und wird zentral über die pyproject.toml konfiguriert.
Linter und Formatter sind nicht dasselbe
Nehmen wir diesen absichtlich unsauberen Code:
def status(name,hp,max_hp=100):
if name == None:
name='Unbekannt'
return f'{name}|HP:{hp}/{max_hp}'
Ein Formatter kümmert sich um die Darstellung:
def status(name, hp, max_hp=100):
if name == None:
name = "Unbekannt"
return f"{name}|HP:{hp}/{max_hp}"
Er korrigiert unter anderem Einrückungen, Leerzeichen, Anführungszeichen und Zeilenumbrüche.
Der Vergleich
name == None
bleibt dabei erhalten. Ob dieser Code pythonic oder möglicherweise problematisch ist, gehört nicht zur Aufgabe des Formatters.
Darum kümmert sich der Linter. Er meldet beispielsweise, dass ein Vergleich mit
None normalerweise mit is geschrieben werden sollte:
name is None
Ruff trennt beide Aufgaben auch auf der Kommandozeile:
ruff check .
ruff format .
ruff check ist der Linter, ruff format der Formatter.
Was Ruff abdeckt
Der Ruff-Linter enthält Neuimplementierungen von Regeln aus einer ganzen Reihe bekannter Python-Tools und Flake8-Plugins, darunter:
- Pyflakes
- pycodestyle
- isort
- pyupgrade
- flake8-bugbear
- flake8-simplify
- pydocstyle
- autoflake
- viele weitere Flake8-Plugins
Die einzelnen Regeln werden dabei direkt in Ruff implementiert. Ruff lädt also nicht intern Flake8 samt einer Sammlung von Python-Plugins.
Der Formatter ist als weitgehend Black-kompatibler Formatter konzipiert.
Weitgehend kompatibel bedeutet allerdings nicht identisch. Ruff besitzt bewusste Abweichungen von Black. Ein Projekt sollte deshalb nicht bei jedem zweiten Speichern Black und danach noch Ruff über dieselben Dateien laufen lassen.
Entscheide Dich für einen Formatter und verwende ihn im gesamten Projekt konsistent.
Warum Ruff so schnell ist
Viele klassische Python-Tools sind selbst in Python geschrieben und analysieren denselben Quelltext unabhängig voneinander.
Ruff ist dagegen in Rust implementiert und bündelt sehr viele Analysen in einem Werkzeug. Zusätzlich verwendet es einen Cache für bereits analysierte Dateien.
Bei einem einzelnen kleinen Skript merkst Du davon kaum etwas. Relevant wird die Geschwindigkeit, wenn der Linter bei jedem Speichern im Editor, bei jedem Commit oder über ein großes Repository läuft.
Dann ist es angenehm, wenn eine vollständige Prüfung eher Teil des normalen Workflows als eine bewusst gestartete Aufgabe wird.
Ruff installieren
Wenn Ruff zu einem konkreten Projekt gehört, solltest Du es auch als Development Dependency dieses Projekts verwalten.
Mit uv:
uv add --dev ruff
Danach kannst Du es über die Projektumgebung ausführen:
uv run ruff --version
Verwendest Du klassisches pip, kannst Du Ruff in der aktiven Virtual
Environment installieren:
python -m pip install ruff
Danach:
ruff --version
Wenn Du Ruff lieber unabhängig von einem Projekt als Kommandozeilenwerkzeug
installieren möchtest, geht das mit uv ebenfalls:
uv tool install ruff
Für reproduzierbare Projekte ist eine Development Dependency meist die interessantere Variante, weil deren Version über den Lockfile festgelegt werden kann.
In den folgenden Beispielen verwende ich direkt ruff. Bei einem mit uv
verwalteten Projekt kannst Du entsprechend uv run ruff einsetzen.
Ein erstes Beispiel
Lege eine Datei beispiel.py an:
import os
import sys
def status(name,hp,max_hp=100):
debug = "temporär"
if name == None:
name='Unbekannt'
return f'{name}|HP:{hp}/{max_hp}'
Lass Ruff darüberlaufen. Die Regel E711 für Vergleiche mit None gehört
seit Ruff 0.16 nicht mehr zum Default-Regelsatz, deshalb schalten wir sie hier
ausdrücklich dazu:
ruff check --extend-select E711 beispiel.py
Du erhältst unter anderem Meldungen wie:
F401 `os` imported but unused
F401 `sys` imported but unused
F841 Local variable `debug` is assigned to but never used
E711 Comparison to `None` should be `cond is None`
Jede Meldung besitzt einen Rule Code wie F401, F841 oder E711.
Zu einer Regel kannst Du Dir direkt im Terminal die Dokumentation anzeigen:
ruff rule F401
Oder:
ruff rule E711
Ruff erklärt dann, was die Regel prüft, warum die gefundene Stelle problematisch sein kann und ob ein automatischer Fix existiert.
Automatische Fixes
Viele Verstöße kann Ruff selbst korrigieren:
ruff check --fix beispiel.py
Das bedeutet allerdings nicht, dass Ruff blind alles verändert, wofür theoretisch ein Fix existiert.
Ruff unterscheidet zwischen safe und unsafe fixes.
Safe Fixes
Safe Fixes sollen die Bedeutung des Programms erhalten.
Das Entfernen eines eindeutig unbenutzten Imports ist beispielsweise in vielen Fällen ein sicherer Fix:
import os
wenn os nirgends verwendet wird.
ruff check --fix wendet standardmäßig nur solche als sicher eingestuften
Korrekturen an.
Unsafe Fixes
Bei manchen Änderungen kann Ruff nicht garantieren, dass sich das Verhalten des Programms nicht verändert.
Unser Vergleich:
name == None
ist ein gutes Beispiel.
Die übliche Python-Schreibweise lautet:
name is None
Trotzdem sind beide Ausdrücke technisch nicht für jedes denkbare Objekt
äquivalent. Eine Klasse kann __eq__() überschreiben und damit das Verhalten
von == beeinflussen.
Deshalb besitzt E711 zwar einen automatischen Fix, Ruff stuft ihn aber als
unsafe ein.
Dasselbe gilt für das Entfernen mancher unbenutzter Zuweisungen. Auch dabei könnten beispielsweise Seiteneffekte oder Kommentare verloren gehen.
Unsafe Fixes kannst Du ausdrücklich zulassen:
ruff check --extend-select E711 --fix --unsafe-fixes beispiel.py
Damit sagst Du Ruff bewusst, dass auch potenziell verhaltensändernde Korrekturen durchgeführt werden dürfen.
Gerade bei einem größeren bestehenden Projekt sollte danach ein:
git diff
selbstverständlich sein.
Für den normalen Alltag ist:
ruff check --fix .
der deutlich konservativere Ausgangspunkt.
Code formatieren
Lint-Fixes und Formatierung sind zwei getrennte Schritte.
Den Formatter startest Du mit:
ruff format beispiel.py
Nach ruff check --fix --unsafe-fixes (mit aktiviertem E711) und
anschließendem ruff format sieht unser Beispiel so aus:
def status(name, hp, max_hp=100):
if name is None:
name = "Unbekannt"
return f"{name}|HP:{hp}/{max_hp}"
Ein ganzes Projekt formatierst Du mit:
ruff format .
Ruff sucht dabei rekursiv nach unterstützten Dateien und berücksichtigt seine Konfiguration sowie Ausschlussregeln.
Nur prüfen, ohne Dateien zu verändern
In einer CI-Pipeline soll Ruff normalerweise keine Dateien reparieren. Die Pipeline soll melden, dass etwas nicht stimmt.
Für den Linter genügt:
ruff check .
Ohne --fix verändert dieses Kommando keine Dateien.
Für den Formatter gibt es:
ruff format --check .
Auch dabei wird nichts geändert. Ruff liefert lediglich einen Fehlerstatus, wenn mindestens eine Datei anders formatiert werden müsste.
Die typische CI-Prüfung besteht damit aus:
ruff check .
ruff format --check .
Ein sinnvoller lokaler Workflow
Während der Entwicklung möchte ich die ungefährlichen Dinge normalerweise direkt korrigieren lassen:
ruff check --fix .
ruff format .
Danach kann ein weiterer Check zeigen, ob Probleme übrig geblieben sind, für die Ruff keinen automatischen Safe Fix angewendet hat:
ruff check .
Der komplette Ablauf wäre damit:
ruff check --fix .
ruff format .
ruff check .
Der letzte Aufruf ist nicht immer nötig. Im Editor oder über pre-commit läuft der Linter häufig ohnehin noch einmal.
Die Reihenfolge von Fixes und Formatierung ist dagegen sinnvoll: Ein Lint-Fix kann Code verändern, den der Formatter anschließend wieder sauber in Form bringt.
Der Formatter sortiert keine Imports
Ein häufiger Irrtum ist:
ruff format .
würde nebenbei auch Imports sortieren.
Das tut der Formatter bewusst nicht.
Die Import-Sortierung gehört zum Linter und steckt in der Rule Family I, die
Ruffs Implementierung der isort-Regeln enthält.
Gezielt sortieren kannst Du mit:
ruff check --select I --fix .
Seit Ruff 0.16 gehören die I-Regeln zum Default-Regelsatz. Dann reicht:
ruff check --fix .
Anschließend:
ruff format .
Import-Sortierung ist also ein Fix des Linters, keine Funktion des Formatters.
Konfiguration in pyproject.toml
Ruff versteht drei Konfigurationsdateien:
pyproject.tomlruff.toml.ruff.toml
In einem normalen Python-Projekt ist pyproject.toml meist ohnehin vorhanden.
Eine sinnvolle Ausgangsbasis könnte so aussehen:
[project]
requires-python = ">=3.12"
[tool.ruff]
line-length = 88
[tool.ruff.lint]
extend-select = [
"E711",
"SIM",
]
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
line-ending = "auto"
Dabei sind einige Einstellungen bewusst explizit angegeben, obwohl sie bereits Ruffs Defaults entsprechen. In einer echten Konfiguration kannst Du solche Zeilen natürlich weglassen.
Nicht mehr jede Grundregel selbst auswählen
Ältere Ruff-Konfigurationen beginnen häufig mit:
[tool.ruff.lint]
select = [
"E4",
"E7",
"E9",
"F",
]
Das war bis Ruff 0.15 der Default-Regelsatz.
Aktuelle Ruff-Versionen bringen bereits einen deutlich umfangreicheren Default-Regelsatz mit. Für ein neues Projekt kannst Du deshalb zunächst einfach:
ruff check .
verwenden und sehen, was Ruff ohne weitere Rule-Auswahl findet.
Zusätzliche Rule Families ergänzt Du mit extend-select.
Zum Beispiel:
[tool.ruff.lint]
extend-select = [
"SIM",
]
Damit bleiben die Default-Regeln aktiv. SIM ist nur teilweise Default; mit
extend-select schaltest Du die restlichen Regeln der Family ein.
select und extend-select sind nicht dasselbe
Der Unterschied ist wichtig.
[tool.ruff.lint]
select = ["F"]
bedeutet sinngemäß:
Verwende
Fals neuen Ausgangspunkt für die aktiven Regeln.
Die Ruff-Defaults werden damit ersetzt.
Dagegen bedeutet:
[tool.ruff.lint]
extend-select = ["N"]
sinngemäß:
Behalte die vorhandenen Regeln und schalte zusätzlich
Nein.
Für ein neues Projekt, das auf Ruffs Default-Auswahl aufbauen soll, ist
extend-select deshalb meistens die passendere Einstellung.
Eine explizite select-Liste bleibt sinnvoll, wenn Du ganz bewusst festlegen
möchtest, welche Rule Families das Projekt verwendet.
Zum Beispiel:
[tool.ruff.lint]
select = [
"E4",
"E7",
"E9",
"F",
"I",
"UP",
"B",
"SIM",
"RUF",
]
Dann ist die Auswahl unabhängig davon, welche zusätzlichen Regeln Ruff in einer späteren Version standardmäßig aktiviert.
Python-Version festlegen
Ruff muss wissen, welche Python-Version Dein Projekt mindestens unterstützt.
Früher wurde dafür oft direkt gesetzt:
[tool.ruff]
target-version = "py312"
Das ist weiterhin gültig.
Wenn Dein Projekt aber bereits seine Python-Version über die standardisierten Projektmetadaten festlegt:
[project]
requires-python = ">=3.12"
kann Ruff daraus in vielen normalen Konfigurationen automatisch ableiten, dass Python 3.12 die Mindestversion ist.
Das ist meist die schönere Lösung, weil requires-python auch von anderen
Python-Werkzeugen verstanden wird.
Eine explizite:
target-version = "py312"
kann trotzdem sinnvoll sein, beispielsweise bei einer reinen
ruff.toml-Konfiguration oder wenn Ruff bewusst eine andere Zielversion
verwenden soll.
Sind beide Werte angegeben, hat target-version für Ruff Vorrang.
Warum die Zielversion wichtig ist
Die Python-Version beeinflusst unter anderem, welche Modernisierungen Ruff vorschlagen darf.
Wenn Dein Projekt noch Python 3.9 unterstützt, sollte ein automatischer Fix nicht plötzlich Syntax erzeugen, die erst ab Python 3.12 funktioniert.
Genau dafür benötigt Ruff diese Information.
Rule Families
Ruff besitzt inzwischen sehr viele Regeln. Sie sind nach Präfixen gruppiert.
Ein paar häufig verwendete Families:
| Präfix | Ursprung beziehungsweise Zweck |
|---|---|
E |
pycodestyle |
F |
Pyflakes |
I |
Import-Sortierung nach isort |
UP |
Modernisierung älterer Python-Schreibweisen |
B |
flake8-bugbear |
SIM |
flake8-simplify |
RUF |
Ruff-eigene Regeln |
N |
pep8-naming |
D |
pydocstyle |
S |
flake8-bandit |
Ein Rule Code bezeichnet anschließend eine konkrete Regel.
F401
F401
Ein Import wird nicht verwendet:
import os
F821
F821
Ein Name ist nicht definiert:
print(spielr)
wenn eigentlich spieler gemeint war.
I001
I001
Ein Import-Block entspricht nicht der konfigurierten Import-Sortierung.
UP015
UP015
Ein überflüssiges Mode-Argument von open() kann entfernt werden:
open("daten.txt", "r")
wird zu:
open("daten.txt")
B006
B006 warnt vor mutablen Default-Werten:
def fuege_hinzu(gegenstand, inventar=[]):
inventar.append(gegenstand)
Dasselbe Problem hatten wir bereits in Teil 5.
Die Liste wird nur einmal beim Definieren der Funktion erzeugt und anschließend über mehrere Funktionsaufrufe hinweg wiederverwendet.
Nicht einfach ALL einschalten
Ruff besitzt den Selector:
[tool.ruff.lint]
select = ["ALL"]
Das klingt zunächst attraktiv: Wenn Ruff eine Regel kennt, soll es sie eben anwenden.
In der Praxis ist das selten ein guter Einstieg.
Die Regelmenge enthält unterschiedliche Philosophien und Anforderungen. Manche Rules verlangen beispielsweise Docstrings, andere besonders strenge Type Annotations oder Regeln, die für ein bestimmtes Framework gedacht sind.
Es gibt außerdem Rule-Kombinationen, die sich mit einem Formatter beißen können.
Für ein neues Projekt würde ich deshalb zunächst die Ruff-Defaults verwenden und nur bewusst weitere Families ergänzen:
[tool.ruff.lint]
extend-select = ["SIM"]
Wenn später ein konkreter Bedarf entsteht, kannst Du beispielsweise N, D,
S oder andere Gruppen ergänzen.
Linter und Formatter aufeinander abstimmen
Nicht jede denkbare Lint-Regel ergibt zusammen mit einem automatischen Formatter Sinn.
Ruff dokumentiert deshalb eine Reihe von Regeln, die mit dem Formatter in Konflikt geraten können. Dazu gehören beispielsweise bestimmte Regeln für Einrückung, Quotes und Trailing Commas.
Ein typisches Beispiel ist E501 für zu lange Zeilen.
Der Formatter versucht, sich an:
[tool.ruff]
line-length = 88
zu orientieren.
Das ist aber keine harte Garantie. Manche Strings, URLs oder andere Konstrukte lassen sich nicht sinnvoll umbrechen.
Wenn Du zusätzlich E501 aktivierst:
[tool.ruff.lint]
extend-select = ["E501"]
kann deshalb auch korrekt formatierter Code noch einen E501-Fehler erzeugen.
Das muss nicht falsch sein. Man sollte nur wissen, dass Formatter und Line-Length-Linter unterschiedliche Aufgaben haben.
Regeln gezielt ignorieren
Nicht jede Meldung ist in jedem Projekt sinnvoll.
Eine bestimmte Meldung in einer Zeile ignorieren
Dafür gibt es # noqa:
import optionales_modul # noqa: F401
Damit ignorierst Du ausschließlich F401 an dieser Stelle.
Auch das hier funktioniert:
import optionales_modul # noqa
Die breite Variante unterdrückt allerdings alle passenden Meldungen dieser Zeile.
Der konkrete Rule Code ist deshalb normalerweise besser. Dadurch ist später noch erkennbar, warum die Ausnahme existiert.
Eine Regel projektweit ignorieren
In der pyproject.toml:
[tool.ruff.lint]
ignore = ["E501"]
Damit ist E501 für das gesamte Projekt deaktiviert.
Projektweite Ausnahmen würde ich nur einsetzen, wenn die Regel tatsächlich nicht zur gewünschten Projektkonvention passt.
Ausnahmen für bestimmte Dateien
Manche Regeln sind grundsätzlich sinnvoll, passen aber nicht auf jeden Dateityp.
Zum Beispiel:
[tool.ruff.lint.per-file-ignores]
"tests/*" = ["S101"]
Damit könnte eine Regel gezielt nur für Testdateien ignoriert werden.
Bei __init__.py stößt man häufig auf bewusst importierte Namen, die nach außen
re-exportiert werden.
Statt dafür pauschal F401 abzuschalten, kannst Du den Re-Export ausdrücklich
schreiben:
from .spieler import Spieler as Spieler
Die Wiederholung zeigt statischen Werkzeugen, dass Spieler absichtlich zur
öffentlichen API des Packages gehört.
Formatter konfigurieren
Der Ruff-Formatter ist absichtlich deutlich weniger konfigurierbar als manche anderen Formatter.
Eine mögliche Konfiguration:
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
line-ending = "auto"
Das sind gleichzeitig die jeweiligen Defaults.
Anführungszeichen
Mit:
quote-style = "double"
bevorzugt Ruff:
name = "Karl"
Stellst Du um auf:
quote-style = "single"
wird normalerweise:
name = 'Karl'
bevorzugt.
Der Formatter kann von dieser Präferenz abweichen, wenn dadurch beispielsweise unnötige Escape-Sequenzen vermieden werden.
Einrückung
Der Default:
indent-style = "space"
verwendet Leerzeichen.
Alternativ versteht Ruff:
indent-style = "tab"
Die übliche Python-Konvention bleibt eine Einrückung mit vier Leerzeichen.
Code in Docstrings formatieren
Ruff kann auch Python-Code innerhalb bestimmter Docstring-Formate formatieren:
[tool.ruff.format]
docstring-code-format = true
Unterstützt werden unter anderem Doctest-Beispiele sowie passende Markdown- und reStructuredText-Codeblöcke innerhalb von Docstrings.
Zum Beispiel:
def addiere(a, b):
"""
Beispiel:
```python
ergebnis=addiere(1,2)
```
"""
Mit aktivierter Docstring-Codeformatierung kann Ruff auch den Python-Code im Codeblock formatieren.
Die Funktion ist standardmäßig deaktiviert.
Formatierung gezielt abschalten
Manchmal hat eine manuelle Anordnung tatsächlich einen Zweck.
Dann kannst Du die Formatierung für einen Bereich deaktivieren:
# fmt: off
werte = [
1, 2, 3,
10, 20, 30,
100, 200, 300,
]
# fmt: on
Für einzelne unterstützte Statements gibt es auch:
werte = [1,2,3,4,5] # fmt: skip
Solche Ausnahmen sollten selten bleiben. Wenn in jeder zweiten Datei
# fmt: off steht, verliert der Formatter einen großen Teil seines Nutzens.
Editor-Integration
Ruff bringt einen eigenen Language Server mit:
ruff server
Normalerweise startest Du ihn nicht manuell. Eine Editor-Erweiterung kümmert sich darum.
Der native Ruff-Server kann unter anderem:
- Diagnosen direkt im Editor anzeigen,
- Quick Fixes anbieten,
- Fix-All-Aktionen ausführen,
- Imports organisieren,
- Code mit Ruff formatieren.
Für Visual Studio Code gibt es eine offizielle Ruff-Erweiterung. Andere Editoren können den Server über das Language Server Protocol einbinden.
Die eigentliche Ruff-Konfiguration gehört trotzdem möglichst in die
pyproject.toml. Dann verwenden CLI, Editor, pre-commit und CI dieselben
Regeln.
Ruff ersetzt keinen vollständigen Python Language Server
Der Ruff Language Server konzentriert sich auf die Aufgaben von Ruff.
Für vollständige Autocompletion, Type Checking und Code-Navigation wird er typischerweise zusammen mit einem dafür gedachten Python Language Server verwendet.
Mögliche Werkzeuge dafür sind beispielsweise:
- ty
- Pyright
- basedpyright
- andere Python-Language-Server
Astral entwickelt mit ty selbst einen separaten Type Checker und Language Server.
Ruff und ty bleiben dabei zwei unterschiedliche Werkzeuge:
Ruff
├── Linting
├── Formatierung
└── Lint-Fixes
ty
├── Type Checking
├── Autocompletion
├── Navigation
└── weitere Language-Server-Funktionen
Du kannst beide im selben Projekt verwenden.
Ruff mit pre-commit
Mit pre-commit kann Ruff automatisch vor einem Git-Commit laufen.
Lege dafür .pre-commit-config.yaml an:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.16.6
hooks:
- id: ruff-check
args: [--fix]
- id: ruff-format
Die Version ist absichtlich fest eingetragen. Dadurch ändert sich der Formatter nicht nur deshalb, weil zufällig eine neue Ruff-Version veröffentlicht wurde.
Aktiviere die Hooks:
pre-commit install
Und teste sie einmal über das gesamte Repository:
pre-commit run --all-files
Die Reihenfolge der Hooks ist bei aktiviertem --fix sinnvoll:
ruff-check --fix
↓
ruff-format
Der Linter kann Code verändern und Imports sortieren. Danach formatiert Ruff das Ergebnis.
Wenn ein pre-commit Hook Dateien verändert, wird der Commit normalerweise
zunächst abgebrochen. Du prüfst die Änderungen, nimmst sie mit git add erneut
auf und startest den Commit noch einmal.
Ruff in der CI
In der CI soll Ruff keine Reparaturen durchführen.
Dort reicht:
ruff check .
ruff format --check .
Mit uv:
uv run ruff check .
uv run ruff format --check .
Findet der Linter einen Verstoß oder erkennt der Formatter eine notwendige Änderung, liefert Ruff einen Fehlerstatus und die Pipeline schlägt fehl.
Damit gilt dieselbe Konfiguration lokal und in der CI.
Ruff in ein bestehendes Projekt einführen
In einem älteren Projekt würde ich Ruff nicht mit allen verfügbaren Regeln und einer vollständigen Neuformatierung gleichzeitig loslassen.
Das produziert vor allem einen riesigen Diff.
1. Sauberen Ausgangspunkt schaffen
Prüfe zuerst:
git status
Offene fachliche Änderungen sollten möglichst vorher committed oder anderweitig gesichert sein.
2. Erst einmal nur prüfen
Starte mit:
ruff check .
So siehst Du, was die Default-Regeln im bestehenden Code finden.
3. Safe Fixes anwenden
Danach:
ruff check --fix .
Und:
git diff
So kannst Du genau nachvollziehen, was Ruff geändert hat.
4. Formatierung separat durchführen
Erst danach:
ruff format .
Eine projektweite Neuformatierung gehört am besten in einen eigenen Commit.
Dann verschwinden spätere fachliche Änderungen nicht in tausenden geänderten Leerzeichen und Zeilenumbrüchen.
5. Zusätzliche Rule Families einschalten
Weitere Rule Families kannst Du danach gezielt ergänzen:
[tool.ruff.lint]
extend-select = [
"SIM",
]
Wenn das Projekt damit sauber ist, können weitere Gruppen folgen.
6. Alte Tools erst danach entfernen
Black, isort, Flake8 oder andere bestehende Werkzeuge solltest Du erst entfernen, wenn Ruff ihre tatsächlich benötigten Aufgaben übernommen hat.
Denke dabei nicht nur an die Development Dependencies. Prüfe auch:
- Editor-Konfiguration
- pre-commit Hooks
- CI
- Makefiles oder Task Runner
- Dokumentation
- alte Tool-Konfigurationen
Sonst läuft nachher irgendwo doch noch Black oder isort über denselben Code.
Ruff-Versionen festlegen
Ruff ist auch in der Version 0.16 noch nicht bei einer stabilen 1.0-API angekommen.
Das Projekt verwendet deshalb ein eigenes Versionierungsschema: Änderungen in der Minor-Version dürfen Breaking Changes enthalten, während Patch-Releases für Bugfixes gedacht sind.
Für reproduzierbare Ergebnisse solltest Du die verwendete Ruff-Version deshalb nicht dem Zufall überlassen.
Mit uv landet die aufgelöste Development Dependency im Lockfile:
uv add --dev ruff
Bei pre-commit wird die Version über rev festgelegt:
rev: v0.16.6
In einer Requirements-Datei wäre eine exakte Version möglich:
ruff==0.16.6
Ruff kann seine eigene Version prüfen
Zusätzlich besitzt Ruff die Einstellung required-version.
Zum Beispiel:
[tool.ruff]
required-version = "==0.16.6"
Startet jemand das Projekt versehentlich mit einer anderen Ruff-Version, beendet Ruff sich mit einem Fehler, statt möglicherweise ein anderes Ergebnis zu produzieren.
Das kann besonders praktisch sein, wenn Entwickler Ruff global installiert haben und nicht garantiert ist, dass jeder tatsächlich die Projektumgebung verwendet.
Wenn Ruff ohnehin konsequent über:
uv run ruff
und einen Lockfile ausgeführt wird, ist diese zusätzliche Absicherung weniger wichtig.
Preview-Regeln
Ruff unterscheidet zwischen stabilen Regeln und Funktionen, die sich noch im Preview-Status befinden.
Preview kannst Du ausdrücklich aktivieren:
[tool.ruff]
preview = true
beziehungsweise für den Linter gezielt:
[tool.ruff.lint]
preview = true
Damit können Regeln und Fixes aktiv werden, die sich noch verändern.
Für ein Lernprojekt oder wenn Du neue Ruff-Funktionen ausprobieren möchtest, kann das interessant sein.
In einem Projekt, das möglichst stabile und reproduzierbare Ergebnisse benötigt, würde ich Preview dagegen nur bewusst einschalten.
Der Ruff-Cache
Ruff speichert seinen Cache standardmäßig in:
.ruff_cache/
Der Ordner enthält keine wertvollen Projektdaten und kann jederzeit gelöscht werden.
Er gehört normalerweise nicht ins Repository:
.ruff_cache/
Wenn Du ihn löschst, analysiert Ruff die benötigten Dateien beim nächsten Lauf erneut.
Was Ruff nicht übernimmt
Ruff deckt einen großen Teil des klassischen Python-Toolings ab, aber längst nicht alles.
Kein vollständiges Type Checking
Ruff versteht Python-Code und Type Annotations gut genug für zahlreiche Lint-Regeln. Das macht ihn aber nicht zu einem vollständigen Type Checker.
Dafür verwendest Du beispielsweise:
- ty
- mypy
- Pyright
- basedpyright
Ein Type Checker kann Fehler erkennen, die ein normaler Linter nicht finden soll:
def verdopple(zahl: int) -> int:
return zahl * 2
verdopple("Dungeon")
Ob und wie streng das beanstandet wird, ist Aufgabe des Type Checkers.
Keine Tests
Ruff führt Deine eigentliche Programmlogik nicht aus und ersetzt weder pytest
noch unittest.
Dieser Code ist hervorragend formatiert:
def addiere(a, b):
return a - b
Der Linter kann ebenfalls vollkommen zufrieden damit sein.
Die Funktion ist trotzdem falsch, wenn sie ihrem Namen entsprechend addieren soll.
Dafür brauchst Du Tests.
Keine Garantie für fehlerfreien Code
Ein Linter kann nur Probleme erkennen, für die er eine passende statische Regel besitzt.
Ruff kann viele Fehler finden, bevor das Programm überhaupt gestartet wird. Es kann aber weder die fachliche Absicht des Programms erraten noch jeden möglichen Runtime-Fehler vorhersagen.
Stolperfallen
-
ruff checkundruff formatverwechseln: Linter und Formatter sind getrennte Kommandos. -
Import-Sortierung vom Formatter erwarten:
ruff formatsortiert keine Imports. Dafür ist dieI-Rule-Family des Linters zuständig. -
selectundextend-selectverwechseln:selectsetzt die Auswahl neu,extend-selectergänzt die bestehende Rule-Auswahl. -
Black und Ruff abwechselnd formatieren lassen: Die Formatter sind weitgehend kompatibel, aber nicht identisch.
-
--fixfür vollkommen risikofrei halten: Standardmäßig werden zwar nur Safe Fixes angewendet, aber Änderungen sollten trotzdem über Git nachvollziehbar bleiben. -
--unsafe-fixesunbesehen verwenden: Diese Fixes dürfen das Laufzeitverhalten verändern oder Informationen wie Kommentare entfernen. -
--fixmit Formatierung verwechseln: Lint-Fixes ersetzenruff formatnicht. -
ALLungeprüft aktivieren: Nicht jede Ruff-Regel passt zu jedem Projekt, und einige Regeln verfolgen bewusst unterschiedliche Stilentscheidungen. -
Eine falsche Python-Zielversion verwenden: Ruff kann sonst Syntax vorschlagen, die auf einer tatsächlich noch unterstützten Python-Version nicht funktioniert.
-
line-lengthals harte Grenze verstehen: Der Formatter versucht die gewünschte Breite einzuhalten, kann aber nicht jede Zeile sinnvoll umbrechen. -
Regeln pauschal mit
# noqaabschalten: Ein konkretes# noqa: F401dokumentiert die Ausnahme besser und versteckt keine zusätzlichen Meldungen. -
Ruff nur global installieren: Dann können Entwickler, Editor und CI unterschiedliche Versionen verwenden.
-
Eine große Neuformatierung mit fachlichen Änderungen mischen: Getrennte Commits machen Reviews und spätere Fehlersuche erheblich angenehmer.
Ein sinnvoller Minimal-Workflow
Für ein neues Projekt braucht es gar nicht viel.
In der pyproject.toml:
[project]
requires-python = ">=3.12"
[tool.ruff]
line-length = 88
[tool.ruff.lint]
extend-select = [
"E711",
"SIM",
]
Lokal korrigieren:
ruff check --fix .
ruff format .
Vor dem Commit oder in der CI prüfen:
ruff check .
ruff format --check .
Und falls pre-commit verwendet wird:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.16.6
hooks:
- id: ruff-check
args: [--fix]
- id: ruff-format
Damit übernimmt Ruff die Routinearbeit: typische Fehler finden, sichere Korrekturen anwenden, Imports sortieren und den Code einheitlich formatieren.
Welche zusätzlichen Regeln ein Projekt benötigt, kannst Du danach in Ruhe entscheiden. Der Default-Regelsatz ist inzwischen gut genug, dass man nicht mehr mit einer halben Seite Konfiguration anfangen muss.
0 Kommentare
Noch keine Kommentare. Sei der/die Erste!
Anmelden um einen Kommentar zu hinterlassen.