07
.
07
.
2025

Idiomatisches Python: Warum guter Code mehr ist als saubere Formatierung

Guter Python-Code benötigt mehr als PEP8: Wie idiomatischer Stil Lesbarkeit und Wartbarkeit verbessert.

Verfasst von:  

Adrien Wehrung | Software-Architekt

Wenn über Code-Qualität gesprochen wird, fällt ein Begriff fast immer zuerst: Styling. Keiner kann behaupten, qualitativ hochwertigen Code zu schreiben, ohne sich an konsistente Formatierungsrichtlinien, sogenannte Code-Style-Richtlinien, zu halten. Doch was oft vergessen wird: Das ist nur der erste Schritt. Um von guter Code-Qualität wirklich zu profitieren, z. B. von besserer Lesbarkeit, Wartbarkeit, Testbarkeit und einer langfristig höheren Entwicklungsgeschwindigkeit, ist mehr nötig. Die gute Nachricht: Das bedeutet nicht automatisch hohen Aufwand. Vielmehr geht es darum, gute Gewohnheiten zu entwickeln und dem Code die richtigen Fragen zu stellen.

Um diese Prinzipien anhand eines konkreten Beispiels zu veranschaulichen, nutzen wir historisches Material: Vor zehn Jahren hielt der Python-Experte Raymond Hettinger eine sehr beliebte Session auf der PyCon2015. Darin zeigt er eindrucksvoll, wie Code-Qualität weit über reine Style-Guides hinausgeht und wie mächtig idiomatischer Code sein kann. Seit dieser Konferenz ist schon eine Ewigkeit vergangen, dennoch sind seine Ideen aus dieser Session aktueller denn je und problemlos auch auf andere Sprachen übertragbar.

In diesem Artikel fassen wir für Dich die Erkenntnisse jener Session zusammen – mit aktualisierten Code-Beispielen für Python 3 (damals war noch Python 2 state-of-the-art). Das Video zu seiner Session findest Du hier: YouTube - Raymond Hettinger - Beyond PEP 8

Oberflächliche Verbesserung

Die Geschichte startet wie folgt: Wir haben den folgenden Code-Review-Kommentar von unserem Chef bekommen.

"Gutes Exception-Handling und Logging 👍

Code-Formatierung bitte nachziehen (PEP 8), das liest sich aktuell ein bisschen schwer

Sollten wir das als Template für andere kleine Skripte nutzen, die mit NetworkElement arbeiten?"

 

Der Code dazu:

import jnettool.tools.elements.NetworkElement, \
       jnettool.tools.Routing, \
       jnettool.tools.RouteInspector

ne=jnettool.tools.elements.NetworkElement( '171.0.2.45' )
try:
    routing_table=ne.getRoutingTable()  # fetch table

except jnettool.tools.elements.MissingVar:
  # Record table fault
  logging.exception( '''No routing table found''' )
  # Undo partial changes
  ne.cleanup( '''rollback''' )

else:
    num_routes=routing_table.getSize()   # determine table size
    for RToffset in range ( num_routes ):
           route=routing_table.getRouteByIndex( RToffset )
           name=route.getName()       # route name
           ipaddr=route.getIPAddr()          # ip address
           print( '%15s -> %s'% (name,ipaddr) ) # format nicely
    ne.cleanup( '''commit''' ) # lockin changes
finally:
    ne.disconnect()

Als Erstes sollten wir sicherstellen, dass wir verstehen, was der Code macht. Es handelt sich um ein kleines Skript, das eine Library nutzt, um bestimmte Routing-Tables zu einer IP-Adresse anzuzeigen. Es verfügt über rudimentäres Fehlerhandling.

Nun zur Bitte aus dem Code-Review: Code-Formatierung nachziehen. Kein Problem, wir können das Code-Styling anpassen. Das sollte den Code deutlich lesbarer machen. Die PEP8-Richtlinie gibt uns einfache Aktionspunkte:

  1. Imports aufräumen → Zwei der drei Imports werden im Code nicht verwendet. Diese sollten wir entfernen.
  2. Variablenbenennung → Die Konvention für Variablennamen in Python ist snake_case. Die Methoden aus der Library können nicht angepasst werden, aber unsere eigenen Variablen schon.
  3. Konsistente Nutzung der Leerzeichen bei Zuweisungen und Funktionsaufrufen → Leerzeichen vor und nach dem "=", keine Leerzeichen nach "(" und vor ")". "y=f( x )" wird zu "y = f(x)".
  4. Konsistente Einrückung Überall vier Spaces nutzen. So heben sich die unterschiedlichen Logik-Blöcke besser ab.
  5. Nutzlose Kommentare entfernen → Die Kommentare im Code-Beispiel wiederholen lediglich die Code-Zeile ohne zusätzlichen Mehrwert. Diese sollten wir entfernen.
  6. String-Formatierung → String-Blöcke für Inline-Strings ergeben keinen Sinn. Wir stellen alles auf normale Strings mit Double-Quote um. Beim Print-Aufruf sollten wir f-Strings statt veralteter Syntax nutzen.

So erhalten wir nun folgenden Code:

import jnettool.tools.elements.NetworkElement

ne = jnettool.tools.elements.NetworkElement("171.0.2.45")

try:
    routing_table = ne.getRoutingTable()
except jnettool.tools.elements.MissingVar:
    logging.exception("No routing table found")
    ne.cleanup("rollback")
else:
    num_routes = routing_table.getSize()
    for rt_offset in range(num_routes):
        route = routing_table.getRouteByIndex(rt_offset)
        name = route.getName()
        ipaddr = route.getIPAddr()
        print(f"{name:>15} -> {ipaddr}")
    ne.cleanup("commit")
finally:
    ne.disconnect()

Es sei angemerkt, dass heutzutage alle diese Anpassungen nicht mehr alle händisch vorgenommen werden müssen, sondern in den meisten Python-IDEs nur einen Klick weit entfernt sind.

So ist der Code auf jeden Fall deutlich lesbarer. Doch es ist noch immer kein "guter" Python Code! Er ist weiterhin nicht pythonisch.

Idiomatischer Code

Nur weil Code PEP8-konform ist, ist er noch lange nicht pythonisch. In der Python-Community bezeichnet der Begriff pythonisch (engl. pythonic) Code, der sich an den Idiomen der Sprache orientiert – lesbar, prägnant, elegant. Solcher Code lässt sich typischerweise leichter schreiben und lesen, er "passt gut" in die Gegebenheiten der Programmiersprache. Mehr zu diesem Thema: What is Pythonic? und The Zen of Python

Zurück zu unserem Beispiel:

Wir haben uns bisher nur auf die PEP8-Richtlinie fokussiert und halten diese nun ein. Dennoch eignet sich unser Code nicht gut, um als Template für andere Skripte genutzt zu werden. Zu leicht vergisst man disconnect() oder cleanup(), baut kontraintuitive Iterationen über die Routing-Tables ein oder bekommt unerwartete Exceptions zur Laufzeit, wenn man nicht genau weiß, wie die darunterliegende Library funktioniert und was alles bei der Nutzung zu beachten ist. Wir erinnern uns: dass unser Code als Template verwendet werden kann, war jedoch ein Wunsch aus dem Review unseres Chefs.

Wie kommen wir also nun zur Lösung?

Die Methoden und Struktur unseres Code-Beispiels erinnern stark an Java: tatsächlich ist die "jnettools" Library aus einer Java-Bibliothek konvertiert worden, daher die Java-ähnliche Package-Struktur, die try-catch-finally-Struktur und die Methodennamen in camelCase.

Auf unserer Suche nach Code-Qualität lautet die Frage, die wir uns jetzt stellen sollten: Wie könnte eine "ideale" pythonische Version von diesem Skript aussehen, wenn die Library für Python geschrieben worden wäre und nicht nur von Java konvertiert?

Unser Zielbild könnte zum Beispiel so aussehen:

from nettools import NetworkElement  # 1

with NetworkElement('171.0.2.45') as ne:  # 2
    for route in ne.routing_table:  # 3
        print(f"{route.name:>15} -> {route.ipaddr}")  # 4

Dieser Code hat folgende wesentliche Unterschiede zu unserem:

  1. Import aus einem Modul, ohne überflüssige Subpackages.
  2. Context-Manager (with) für die Behandlung der Verbindung.
  3. Iteration nativ über eine Iterable Property.
  4. Zugriff auf Name und IP-Adresse auch über Property anstatt Getters.

Die nächste Frage, die wir uns nun stellen müssen, ist: Wie erreichen wir dieses Zielbild?

Eine Lösung in diesem Fall ist das Adapter-Pattern. Einen Adapter zuschreiben, der die Nutzung der Library intuitiv und pythonisch macht, ist gar nicht so schwer. Wir starten mit neuen Klassen als Wrapper für die Library-Klassen und definieren auch eine eigene Exception in dem Modul:

import jnettool.tools.elements.NetworkElement

class NetworkElementError(Exception):
    pass

class NetworkElement:
    def __init__(self, ipaddr):
        self.ipaddr = ipaddr
        self._old_ne = jnettool.tools.elements.NetworkElement(ipaddr)

class RoutingTable:
    def __init__(self, old_rt):
        self._old_rt = old_rt

class Route:
    def __init__(self, old_route):
        self._old_route = old_route

So muss der Nutzer unseres Adapters nicht mehr mit den Klassen der ursprünglichen Library interagieren. Alle "old_"-Attribute entsprechen Objekten der ursprünglichen Library. Keines davon gehört zu unserem public Interface Adapter. Vom Zielergebnis haben wir fast die erste Zeile geschafft:

from nettools import NetworkElement

ne = NetworkElement('171.0.2.45')

Nun zum Context-Manager: Für eine angenehmere Behandlung von Ressourcen bietet Python die Nutzung von Context-Managern mit dem "with" Keyword an. Damit eine Klasse als Context-Manager benutzt werden kann, muss sie zwei Methoden definieren. Die erste wird beim Aufmachen des Kontextes aufgerufen (in unserem Fall macht sie nichts) und die zweite beim Schließen. Bei Exceptions bekommt sie die Details (Typ, Instanz, Traceback), um eine saubere Schließung zu gewährleisten. In unserem Fall soll bei Fehlern eine Log-Zeile geschrieben und dann auf die gewrappte Klasse ein Rollback durchgeführt werden, sonst ein Commit, und in jedem Fall ein Disconnect.

Notiz: Alternativ könnte das Context-Management über einen mit @contextlib.contextmanager dekorierten Generator erfolgen. Da hier aber der Kontext auf dem gesamten Objekt angewendet wird, ist der Einsatz von __enter__/__exit__ weniger fehleranfällig.

Damit können wir schon das Context-Handling realisieren:

from nettools import NetworkElement

with NetworkElement('171.0.2.45') as ne:
    pass # TODO: use "ne"

Den Getter auf die Routing-Table können wir durch eine Property ersetzen:

# in class NetworkElement
    @property
    def routing_table(self):
        try:
            return RoutingTable(self._old_ne.getRoutingTable())
        except jnettool.tools.elements.MissingVar:
            raise NetworkElementError("No routing table found")

Diese Syntax erlaubt es, die Routing-Table mit ".routing_table" zu holen, als wäre es ein Attribut. Wird eine Exception beim Aufruf geworfen, so wird der Context-Manager darauf reagieren und regelrecht das Logging und Rollback vor dem Disconnect durchführen.

Soweit haben wir die Möglichkeit geschaffen, die erste Hälfte unseres pythonischen Code zu realisieren:

from nettools import NetworkElement

with NetworkElement('171.0.2.45') as ne:
    ne.routing_table  # TODO: iterate over this

Objekte können in Python "iterable" gemacht werden (sprich, wie eine Liste behandelt), in dem sie zwei Methoden definieren: Eine, um die Länge zu bekommen, und eine, um Elemente nach Index zu holen. In diesen zwei Methoden können wir die ursprünglichen Library-Methoden wrappen:

# in class RoutingTable
    def __len__(self):
        return self._old_rt.getSize()

    def __getitem__(self, index):
        if index >= len(self):
            raise IndexError
        return Route(self._old_rt.getRouteByIndex(index))

Und schon ist es möglich, mit "for ... in ..." über das Objekt zu iterieren:

from nettools import NetworkElement

with NetworkElement('171.0.2.45') as ne:
    for route in ne.routing_table:
        pass # TODO: pretty print entries

Schließlich definieren wir noch Properties für die "Route"-Attribute:

# in class Route
    @property
    def name(self):
        return self._old_route.getName()

    @property
    def ipaddr(self):
        return self._old_route.getIPAddr()

Und unser pythonischer Code ist vollständig funktionsfähig:

from nettools import NetworkElement

with NetworkElement('171.0.2.45') as ne:
    for route in ne.routing_table:
        print(f"{route.name:>15} -> {route.ipaddr}")

Binnen weniger Zeilen Code haben wir eine zwar gültige, aber nicht python-freundliche Library so adaptiert, dass ihre Nutzung jetzt einfach und konform zu den Python-Prinzipien gemacht wurde.

 

Fazit

Durch die Kombination aus Style-Korrekturen und idiomatischer API-Umgestaltung haben wir ein kleines Skript deutlich verbessert – nicht nur in Bezug auf Lesbarkeit, sondern auch auf Wartbarkeit und Wiederverwendbarkeit.

Die ganze ursprüngliche Komplexität, die der Java-Konvertierung verschuldet war, ist nun vom Endnutzer weg abstrahiert. Anstatt der ursprünglichen Library, können wir nun den Adapter mit anderen Entwicklern teilen und nutzen. Die resultierenden vier Code-Zeilen entsprechen wirklich einem Template oder Beispiel, das für andere Skripte dieser Art wieder benutzt werden kann.

Dieses Ergebnis war nur möglich, weil wir die Code-Qualität nicht rein anhand von Style-Richtlinien betrachtet haben, sondern darüber hinaus gedacht haben. Wir haben darauf geachtet, dass wir bei der Programmierung die Idiome unserer Programmiersprache beachten.

 

Was zeigt uns dieses Beispiel?

Code-Qualität beginnt beim Stil, aber sie endet dort nicht. Richtig gute Code-Qualität entsteht, wenn wir die Stärken der Sprache ausschöpfen – mit sauberem Design, sinnvollen Abstraktionen und einem klaren Fokus auf den Entwickler als Leser.