Code-Qualität in Python: Pylint als Linter für Python-Code einsetzen

Wir haben unsere IDE Visual Studio Code dank der Plugins so konfiguriert, dass sie uns während des Schreibens bereits viel Arbeit abnimmt, um unseren Code gemäß des PEP-8-Standards zu formatieren, aber alles kann uns das Plugin nicht abnehmen. Wir sehen uns einmal an, wie wir mit dem Einsatz des Linters Pylint unserem Ziel nach wohlgeformtem Python-Code ein entscheidendes Stück näher kommen.

Höhere Ansprüche

Wir erinnern uns: Beim Einrichten unserer Entwicklungsumgebung (IDE) Visual Studio Code haben wir die beiden Erweiterungen „Pylint“ und „autopep8“ installiert. Pylint zeigt uns während des Codens, was fehlerhaft ist oder sein könnte und autopep8 kümmert sich darum, bei jedem Speichervorgang einen bestimmten Formatierungsstandard einzuhalten, wie beispielsweise die Anzahl der Leerzeichen zwischen Zuweisungen oder die der Leerzeilen zwischen Methoden. Das ist sehr praktisch, aber die Arbeit ist damit nicht getan, wenn wir höheren Ansprüchen genügen wollen.

Was läuft eigentlich schief?

Wir öffnen einmal mehr unser fizzbuzz-Projekt in Visual Studio Code. Unsere erste Tat ist das Aktivieren der virtuellen Umgebung – per Terminal: source fizzbuzz_venv/bin/activate

Lassen wir nun Pylint einmal mit dem Kommando pylint *.py im Terminal von VS Code auf unsere Python-Dateien in unserem fizzbuzz-Verzeichnis los, treten vielerlei Verbesserungsmöglichkeiten zutage:

************* Module FizzBuzz
FizzBuzz.py:1:0: C0103: Module name "FizzBuzz" doesn't conform to snake_case naming style (invalid-name)
FizzBuzz.py:1:0: C0111: Missing module docstring (missing-docstring)
FizzBuzz.py:1:0: C0103: Class name "fizzbuzz" doesn't conform to PascalCase naming style (invalid-name)
FizzBuzz.py:1:0: C0111: Missing class docstring (missing-docstring)
FizzBuzz.py:3:4: C0111: Missing method docstring (missing-docstring)
FizzBuzz.py:3:4: R0201: Method could be a function (no-self-use)
FizzBuzz.py:9:4: C0111: Missing method docstring (missing-docstring)
FizzBuzz.py:9:4: R0201: Method could be a function (no-self-use)
FizzBuzz.py:15:4: C0111: Missing method docstring (missing-docstring)
FizzBuzz.py:15:4: R0201: Method could be a function (no-self-use)
FizzBuzz.py:21:4: C0103: Method name "checkNumber" doesn't conform to snake_case naming style (invalid-name)
FizzBuzz.py:21:4: C0103: Argument name "numberToCheck" doesn't conform to snake_case naming style (invalid-name)
FizzBuzz.py:21:4: C0111: Missing method docstring (missing-docstring)
FizzBuzz.py:22:8: R1705: Unnecessary "elif" after "return" (no-else-return)
************* Module TestFizzBuzz
TestFizzBuzz.py:1:0: C0103: Module name "TestFizzBuzz" doesn't conform to snake_case naming style (invalid-name)
TestFizzBuzz.py:1:0: C0111: Missing module docstring (missing-docstring)
TestFizzBuzz.py:5:0: C0111: Missing class docstring (missing-docstring)
TestFizzBuzz.py:10:4: C0111: Missing method docstring (missing-docstring)
TestFizzBuzz.py:14:4: C0111: Missing method docstring (missing-docstring)
TestFizzBuzz.py:18:4: C0111: Missing method docstring (missing-docstring)
TestFizzBuzz.py:23:4: C0103: Method name "test_checkNumber" doesn't conform to snake_case naming style (invalid-name)
TestFizzBuzz.py:23:4: C0111: Missing method docstring (missing-docstring)

-------------------------------------------------------------------
Your code has been rated at 5.42/10 (previous run: 10.00/10, -4.58)

Sogar eine Bewertung gibt es am Ende: Unser Code erhält für seinen Stil „5.42/10“. Das ist eher wenig beeindruckend, das gilt es zu verbessern. Wir wollen „10.00/10“ erhalten. Der Befehl pylint *.py bedeutet nichts anderes, als dass unser Linter „Pylint“ jede Datei mit der Endung „py“ im aktuellen Verzeichnis prüfen soll.

Mit Pylint zu gut formatiertem Code – Teil 1

Es sind zum Glück nur zwei Dateien, die wir bearbeiten müssen. Die können wir nacheinander anhand der Hinweise von Pylint bearbeiten. Als Grundlage hierfür dient PEP 8 — Style Guide for Python Code, denn dort ist beschrieben, was der Standard für die Formatierung und Namensgebung ist. Fangen wir mit „FizzBuzz.py“ an:

Pylint schreibt, dass in der Datei „FizzBuzz.py“ in Zeile „1“ an der Stelle „0“ ein Problem mit dem Code „C0103“ existiert. Diese Codes lassen sich unter pylint-messages nachsehen. Liest man Pylints Ausgabezeile weiter steht dort, dass der Name des Moduls nicht dem Styleguide entspricht. Sehen wir im PEP-8-Styleguide unter „Package and Module Names“ nach, so liest man dort als erstes „Modules should have short, all-lowercase names“. Das trifft auf unser „FizzBuzz.py“ nicht zu, also müssen wir das ändern. Links im Explorer in VS Code markieren wir die Datei „FizzBuzz.py“ und drücken die Eingabetaste. So wird der Name der Datei bearbeitbar. Wir ändern „FizzBuzz.py“ zu „fizzbuzz.py“ und bestätigen die Änderung mit dem erneuten Drücken der Eingabetaste.

Jetzt müssen wir daran denken, dass wir in „TestFizzBuzz.py“ das Modul „FizzBuzz“ importieren. Daher ändern wir in „TestFizzBuzz.py“ die Zeile import FizzBuzz in import fizzbuzz. Dabei fällt auf, dass wenn der Name „FizzBuzz.py“ ein Problem darstellt, dann wird das auch für „TestFizzBuzz.py“ gelten. Blickt man weiter unten in die Ausgabe von Pylint, steht dort auch unter „Module TestFizzBuzz“ die gleiche Meldung wie unter „Module FizzBuzz“. Also benennen wir fix auf die gleiche Art und Weise „TestFizzBuzz.py“ zu „testfizzbuzz.py“ um. Damit sind wir auch schon fertig, denn „TestFizzBuzz.py“ wurde nirgends importiert.

Kommen wir zu Pylints zweitem Hinweis unter unter „Module FizzBuzz“. Pylint schreibt, dass in der Datei „FizzBuzz.py“ in Zeile „1“ an der Stelle „0“ ein Problem mit dem Code „C0111“ existiert und weist darauf hin, dass das Modul „FizzBuzz“ kein „docstring“ hat. Tatsächlich gibt es im frisch umbenannten „fizzbuzz.py“ keinen Hinweis darauf, was dieses Modul eigentlich tut. In Python verwendet man Block-Kommentare im Format """ ham spam eggs """ für Hinweise an Leserinnen und Leser des Moduls, was der Zweck und die Arbeitsweise des jeweiligen Moduls sind. Wir ergänzen daher in der ersten Zeile von „fizzbuzz.py“ etwas in der Art von """ Dieses Modul dient der Lösung der berühmt-berüchtigten Fizzbuzz-Herausforderung. """.

Im dritten Hinweis von Pylint lesen wir, dass der Name der Klasse nicht dem „PascalCase“ entspricht. Daher ändern wir den Namen der Klasse von „fizzbuzz“ zu „FizzBuzz“.

Der vierte Hinweis bezieht sich abermals auf ein fehlendes „docstring“, dieses Mal allerdings auf die Klasse, denn auch diese sollte ihren eigenen Hinweis haben. Gleich unterhalb der Klassendefiniton ergänzen wir einen Eintrag über den Sinn und Zweck der Klasse. Das ist in diesem Beispiel etwas schwierig, denn die Modulbeschreibung erklärt bereits viel, so dass man in der Klasse folgenden docstring schreiben könnte: """ Diese Klasse dient der Lösung der berühmt-berüchtigten Fizzbuzz-Herausforderung. """.

Auch in der fünften Zeile der Pylint-Meldungen geht es um einen fehlenden „docstring“-Eintrag – nun aber in einer Methode. Aus diesem Wissen heraus ergänzen wir alle vier Methoden der Klasse um einen „docstring“-Eintrag gleich als erste Zeile unter der Methodendeklaration.

Zwischenstand

Nun haben wir bereits einige der durch Pylint angemerkten Hinweise umgesetzt. Zeit, um einmal einen Zwischenstand abzurufen. Dafür führen wir wieder das Kommando pylint *.py aus. Ich erspare alle Hinweise und bilde hier nur die Bewertung ab:

Your code has been rated at 6.04/10

Das ist eine Verbesserung im Vergleich zur vorigen „5.42/10“. Wir sind also auf dem richtigen Weg, denn die Zahl der Hinweise hat sich ja auch verringert.

Mit Pylint zu gut formatiertem Code – Teil 2

Wie der Hinweis zu fehlenden docstrings taucht auch die Meldung „R0201: Method could be a function (no-self-use)“ mehrmals auf; drei Mal um genau zu sein. Das sind die drei Methoden, die als Parameter ein „self“ erwarten, aber anschließend nichts mit dem Objekt machen. Pylint weist darauf hin, dass diese Methoden gar nicht Teil einer Instanz sein müssten. Löschen wir den Self-Parameter und speichern die Änderung, meckert unser Helfer-Plugin mit „Method should have „self“ as first argumentpylint(no-self-argument)“. Was tun? Wir müssen diese drei Methoden als statische Methoden deklarieren. Das tun wir, indem wir über jede der drei Methoden den Hinweis @staticmethod ergänzen.

Als nächstes müssen wir uns um den Methodennamen „checkNumber“ und die dazugehörige Parameterbezeichnung „numberToCheck“ kümmern, denn die entsprechen laut Pylint nicht dem Styleguide. Wir benennen „checkNumber“ zu „check_number“ und „numberToCheck“ zu „number_to_check“ um. Anschließend müssen wir alle Vorkommnisse von „numberToCheck“ innerhalb der Methode ebenfalls zu „number_to_check“ umbenennen. Das geht dank VS Code recht schnell von der Hand: Wir markieren das erste Vorkommen von „numberToCheck“ und wählen im Hauptmenü unter dem Eintrag „Selection“ den Punkt „Add Next Occurrence“. Auf diese Weise kümmert sich VS Code um das Finden und Markieren des nächsten Vorkommens des zuerst markierten Wortes. Es bietet sich an, das Tastenkürzel zu lernen, das hinter dem Punkt „Add Next Occurrence“ angezeigt wird. So geht das Markieren noch schneller. Sind alle Vorkommen ausgewählt, schreiben wir „number_to_check“ und – voilá – alle Parameter sind geändert.

Bleibt als letzter Eintrag beim „Module fizzbuzz“ noch „R1705: Unnecessary „elif“ after „return“ (no-else-return)“ übrig. Pylint bemängelt, dass nach einem Return-Statement noch weitere Optionen mit „elif“ angegeben sind. Wir ändern die beiden „elif“ in „if“ und lassen einmal mehr pylint *.py laufen. Siehe da: Es gibt keine Hinweise zu „Module fizzbuzz“ mehr und unsere Bewertung hat sich auf „7.29/10“ verbessert. Damit sind wir mit der ersten Datei fertig. Beginnen wir nun mit „testfizzbuzz.py“.

Nach dem Öffnen fällt auf, dass die Zeile „self.fizzbuzz = FizzBuzz.fizzbuzz()“ als fehlerhaft angezeigt wird, was auch richtig ist, da sie nach unseren Umbenennungen self.fizzbuzz = fizzbuzz.FizzBuzz() heißen muss, denn das Modul ist nun im snake_case- und die Klasse im PascalCase-Stil. Nun tauchen auch noch mehr Fehler auf, denn die Klasse FizzBuzz hat keine Methode „checkNumber“ mehr – die heißt jetzt „check_number“. Schnell ändern wir dank „Add Next Occurrence“ die Methodenaufrufe in „check_number“ und beseitigen damit sind die entstandenen Fehler.

Wir können uns nun den Pylint-Hinweisen zu „Module testfizzbuzz“ widmen, was an dieser Stelle nicht in der zuvor gezeigten Ausführlichkeit beschrieben wird. Es geht auch in testfizzbuzz um fehlende docstrings und unpassende Methodennamen, also um Themen, die wir bereits in den Erläuterungen zu „fizzbuzz.py“ bearbeitet haben.

Erhalten wir schließlich nach einer Ausführung von pylint *.py

Your code has been rated at 10.00/10

als Bewertung, ist unser Werk vollbracht und unser Code entspricht dem Python-Styleguide. Chacka!

Was ist mit den Tests?

Bevor wir unser überarbeitetes Werk per Git in sein Repository einchecken, vergewissern wir uns, dass unsere Tests und damit auch die zu testende Funktionalität des Moduls „fizzbuzz“ weiterhin fehlerfrei laufen. Wir öffnen „testfizzbuzz.py“ in VS Code und drücken die Tastenkombination [alt]+[cmd]+[n]. Alternativ können wir oben rechts im Fenster das nicht gefüllte Dreieck klicken oder auch im Terminal von VS Code python3 testfizzbuzz.py ausführen.

Laufen unsere Tests fehlerlos durch, sind wir am Ziel und können unsere Verbesserungen ins Repository einchecken.

Fazit

Wir haben gelernt: Es ist sinnvoll, nach Code-Änderungen nicht nur die Tests laufen zu lassen, sondern auch den Linter zu beauftragen, den Code zu prüfen. Auf diese Weise bleiben die notwendigen Änderungen überschaubar, um unseren Code gemäß Standard gepflegt zu halten.

Wäre es nicht wunderbar, wenn wir das Testen und das Prüfen automatisch laufen lassen könnten, ohne dass wir jedes Mal daran denken müssen, diese Schritte auszuführen? Das würde uns helfen, die Code-Qualität hoch zu halten und den menschlichen Faktor des Vergessens zu minimieren.

Genau das ist eine der Hauptideen hinter Continuous Integration (CI).