Zum Inhalt springen

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.toml
  • ruff.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 F als 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 N ein.

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 check und ruff format verwechseln: Linter und Formatter sind getrennte Kommandos.

  • Import-Sortierung vom Formatter erwarten: ruff format sortiert keine Imports. Dafür ist die I-Rule-Family des Linters zuständig.

  • select und extend-select verwechseln: select setzt die Auswahl neu, extend-select ergänzt die bestehende Rule-Auswahl.

  • Black und Ruff abwechselnd formatieren lassen: Die Formatter sind weitgehend kompatibel, aber nicht identisch.

  • --fix für vollkommen risikofrei halten: Standardmäßig werden zwar nur Safe Fixes angewendet, aber Änderungen sollten trotzdem über Git nachvollziehbar bleiben.

  • --unsafe-fixes unbesehen verwenden: Diese Fixes dürfen das Laufzeitverhalten verändern oder Informationen wie Kommentare entfernen.

  • --fix mit Formatierung verwechseln: Lint-Fixes ersetzen ruff format nicht.

  • ALL ungeprü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-length als harte Grenze verstehen: Der Formatter versucht die gewünschte Breite einzuhalten, kann aber nicht jede Zeile sinnvoll umbrechen.

  • Regeln pauschal mit # noqa abschalten: Ein konkretes # noqa: F401 dokumentiert 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.

Weiterlesen

0 Kommentare

Noch keine Kommentare. Sei der/die Erste!