Blog

Wie Sie Ihre Python-Codebasis zähmen

Joachim Bargsten
Arjan Molenaar

Joachim Bargsten, Arjan Molenaar

Aktualisiert Oktober 15, 2025
7 Minuten

Sie fangen ganz klein an, vielleicht mit einem Proof of Concept, einer kleinen Anwendung oder einer Data Engineering Pipeline. Oder Sie wollen ein komplettes Domain Driven Design mit allem Drum und Dran? Früher oder später kommen Sie an den Punkt, an dem Sie feststellen: Ich habe ein Chaos angerichtet oder zumindest dazu beigetragen. Sie ändern eine Sache hier und zehn andere Dinge gehen dort drüben schief. Willkommen in der großen Schlammkugel.

Und Zeit für eines unserer Grundprinzipien:

Globale Organisation, lokales Chaos.

Ihr Projekt hat mehrere Epochen des evolutionären Wachstums hinter sich, von Ihnen und von Entwicklern vor Ihnen. Sie haben Funktionen hinzugefügt. Viele, viele Funktionen, aber niemand hat sich die ganze Sache angesehen. In diesem Stadium sind zwei Dinge offensichtlich:

  1. Sie müssen sich "Flügel wachsen lassen", in die Vogelperspektive wechseln und Grenzen ziehen, d.h. separate Module definieren.
  2. Jetzt, da Sie Ihre Grenzen kennen, müssen Sie dafür sorgen, dass diese Grenzen auch respektiert werden.

Punkt 1 können Sie höchstwahrscheinlich nicht in einem Blogbeitrag lernen, aber Punkt 2 können wir hier definitiv in Angriff nehmen.

Chaotische Codebasen sind schwer zu zähmen, und wenn sie einmal gezähmt sind, ist es schwierig, sie in diesem Zustand zu halten. Als Beispiel nehmen wir eine Richtlinie des Domain Driven Design (oder kurz DDD):

das Domänenmodell hat keine Abhängigkeiten (außer den grundlegendsten und wichtigsten)

Sie können das Domänenmodell als "Modul" sehen, das die gesamte Geschäftslogik enthält. Die Verteilung Ihrer Geschäftslogik auf die Module oder Schichten einer Anwendung ist in 99,9% der Fälle ein Rezept für ein Desaster. Konzeptionell könnte eine Regel zum Schutz unseres Domänenmoduls so geschrieben werden:

  1. für jede Datei im Domänenmodellmodul
  2. Importe aus anderen Modulen verbieten
  3. aber erlauben Sie Importe aus dem Domainmodul selbst

Der Test, um so etwas zu validieren, könnte so geschrieben werden:

from pytest_archon import archrule

def test_domain_model():
    (
        archrule("protect the domain model")
        .match("app.domain_model*")        # (1)
        .should_not_import("app*")         # (2)
        .may_import("app.domain_model*")   # (3)
        .check("app")
    )

Der obige Test ist bereits völlig gültiger pytest-archon Code. Was ist also pytest-archon? Es versucht, Ihnen bei dieser Frage zu helfen:

Wie kann ich die Grenzen kodifizieren, nach denen ich meine Anwendung entwickle und erweitere?

Es handelt sich also um ein Pytest-Plugin, das Ihnen hilft, (architektonische) Regeln für Ihre Anwendung zu definieren (archon bedeutet Lineal, klingt aber auch ein bisschen wie der Bogen in Architektur). Wir haben es auf einem unserer Innovationstage bei Xebia entwickelt. Die Architekturregeln werden in einfachen Pytest-Testfällen definiert und können als Teil einer CI/CD-Pipeline ausgeführt werden. Es kratzt an unserem eigenen Juckreiz: Als Berater wissen wir gleich zu Beginn eines Auftrags, dass unsere Zeit begrenzt sein wird. Wie können wir trotzdem sicherstellen, dass unsere Initiativen und Bemühungen um Codequalität auch nach unserem Ausscheiden erhalten bleiben?

Schützen Sie Ihre Architektur

Traditionell beschäftigen sich Python-Codebasen wenig mit Architekturfragen. Die meisten Anwendungen verwenden ein bereits festgelegtes Framework wie Django oder FastAPI oder haben nicht die Größe, um die Vorteile einer klaren Architektur zu nutzen. Daher wird nur wenig Aufwand in die Architektur gesteckt. Aber wenn Sie wachsen, werden Sie einen Wendepunkt erreichen, an dem Sie von der Beachtung architektonischer Belange stark profitieren.

Gemeinsame Architekturen Abbildung 1: (A) Eine einfache Architektur für eine Python-Web- oder CLI-Anwendung. (B) Bei größeren Anwendungen ist es sinnvoll, eine Diensteschicht als Abstraktion zwischen dem Web- oder CLI-Framework zu schaffen. (C) Dieses Muster ermöglicht es, die Anwendung noch weiter in eine Architektur mit mehreren Modulen zu erweitern.

Ein gängiger Ansatz ist das Model-View-Controller-Designmuster (Abbildung 1A). Als logischen nächsten Schritt könnten Sie eine Diensteschicht hinzufügen , die als Abstraktionsschicht für Ihr Domänenmodell und die Datenbankteile dient (Abbildung 1B). Betrachten Sie die auf einer Dienstschicht basierende Einrichtung als Baustein eines Gerüsts für Ihre Anwendung (Abbildung 1C). Welche Architektur Sie wählen und was für Ihre Anwendung geeignet ist, ist stark kontextabhängig. Für pytest-archon spielt es keine Rolle, was Sie wählen, sondern nur, worauf Sie achten müssen: auf die Abhängigkeiten (rote Pfeile) und auf das Fehlen von Abhängigkeiten (unsichtbare Pfeile). Wie sieht das in pytest-archon aus? Stellen Sie sich vor, Sie bauen eine App zum Buchen von Flugtickets mit den Modulen Bestellung, Preisberechnung und Reservierung.

src
└── flight_ticket
    ├── common
    ├── order
    │   ├── data
    │   └── domain
    ├── price_calculation
    │   ├── data
    │   └── domain
    └── reservation
        ├── data
        └── domain


Für Architektur A (Abbildung 1) müssen Sie nur sicherstellen, dass

  1. das Domänenmodell nicht von anderen Modulen der Anwendung abhängt.
def test_fig1a():
    (
        archrule("fig 1a: domain model has no dependencies")
        .match("flight_ticket.order.domain*")
        .should_not_import("flight_ticket*")
        .may_import("flight_ticket.order.domain*")
        .check("flight_ticket")
    )


Für Architektur B (Abbildung 1) müssen Sie sicherstellen, dass

  1. der Controller, CLI oder andere Module interagieren nur mit der Serviceschicht
  2. das Domänenmodell ist nicht von anderen Modulen der Anwendung abhängig

def test_fig1b1():
    (
        archrule("fig 1b (1): other modules only uses service level")
        .match("flight_ticket*")
        .exclude("flight_ticket.order.*")
        .may_import("flight_ticket.order")
        .should_not_import("flight_ticket.order.*")
        .check("flight_ticket")
    )

Diese Regel verdient eine Erklärung: Ziel ist das Bestellmodul. Wir schließen alle Untermodule flight_ticket.order.* aus, weil sie sich gegenseitig importieren müssen. Alle anderen dürfen das Hauptmodul flight_ticket.order (das die API/den Dienst enthält) importieren, aber nicht die Untermodule flight_ticket.order.*.

Und die zweite Option ist bereits in "Architektur A" beschrieben.


Für Architektur C (Abbildung 1) müssen Sie sicherstellen, dass

  1. für jedes Modul: (a) der Controller (oder CLI) interagiert nur mit der Dienstebene (b) das Domänenmodell ist nicht von anderen Modulen der App abhängig
  2. Module sind nicht voneinander abhängig

Wir werden hier nur die Lösung skizzieren, die Implementierung überlassen wir dem Leser als Übung. Die Idee ist einfach: Iterieren Sie jedes Modul und wenden Sie die gleichen Architekturregeln an.

@pytest.mark.parametrize("module", ['order', 'price_calculation', 'reservation'])
def test_fig1c(module):
    (
        archrule("domain model has no dependencies")
        .match(f"flight_ticket.{module}.domain*")
        .should_not_import("flight_ticket*")
        .may_import(f"flight_ticket.{module}.domain*")
        .check("flight_ticket")
    )

Abhängig von der Struktur Ihrer Anwendung können Sie die zweite Option entweder wie folgt angehen

  1. stellen Sie sicher, dass nur der Controller oder die App ein Modul importiert oder
  2. Wählen Sie ein Modul A und prüfen Sie, ob andere Module B, C Modul A importieren. Dann nehmen Sie das nächste Modul B und prüfen Sie, ob A oder C B importieren usw.

Nebenbemerkung Falls Sie sich fragen: Wie zum Teufel soll ich sicherstellen, dass die Datenbank nur das Domänenmodell verwendet und nicht umgekehrt, können Sie sich vom kosmischen Pythonbuch inspirieren lassen(ORM hängt vom Modell und dem Repository-Musterab ). Je nachdem, was Sie bevorzugen, könnten Sie die Repository-Definition auch in eine Schnittstelle, die in das Domänenmodul eingeht, und eine Implementierung der Schnittstelle, die sich im Daten- oder db-Modul befindet, aufteilen. Das Domänenmodell würde dann ausschließlich die Schnittstelle verwenden. Die Anwendung kann eine Implementierung instanziieren und sie dem Domänenmodell als Argument übergeben, wodurch Domänen- und Datenschicht effektiv entkoppelt werden.

Architektur hat zwei natürliche Feinde: Faulheit und Architektur-Astronauten

Die obigen Regeln sind ziemlich global, und das mit Absicht. Wir wollen nur ein paar Regeln, die Grenzen, in unserer Anwendung definieren. Gerade genug, um die Architektur klar zu halten und Überraschungen zu vermeiden.

Außerdem können diese Regeln auch helfen, wenn Sie Ihre App als Bibliothek verwenden möchten. Wenn Sie denken: Eine Befehlszeilenschnittstelle (CLI) wäre schön, aber ich möchte nicht die ganze Webserver-Maschinerie in Gang setzen, nur um einen einfachen Befehl auszuführen, kann python-archon Ihnen helfen. Sie erstellen Regeln, um den Import von Web-Server-bezogenem Code für Ihre CLI zu verhindern.

python-archon soll eine Leitplanke sein

Um zum Titel dieses Abschnitts zurückzukehren:

Übertreiben Sie es bitte nicht. pytest-archon ist eine Leitplanke!

Betrachten Sie pytest-archon als Leitplanken für (oder gegen?) das Hinunterfahren der Klippe. Tappen Sie nicht in die Falle, jeden Aspekt Ihrer Code-/Modulstruktur festnageln zu wollen.

Das Gegenteil von zu viel Einschränkung ist Faulheit: Sie finden einen Fehler im Domainmodul. Leicht zu lösen, denken Sie: einfach eine Funktion aus dem Datenbankmodul wiederverwenden. Das Datenbankmodul enthält jedoch speziellen Code für die Datenbankkommunikation und sollte nicht überall importiert werden. Der schnelle Hack, das Datenbankmodul in das Domänenmodul zu importieren, würde die Logik von der Datenbank abhängig machen. Sie brauchen nur ein paar dieser "schnellen Korrekturen", um Ihre Codebasis durcheinander zu bringen.

Fazit

pytest-archon ist eine bequeme Möglichkeit, architektonische Grenzen zu schreiben, einfach in Python. Sie müssen keine spezielle Syntax lernen. Keine YAML-Dateien, die für Formatierung und Linting unerreichbar sind. Sie können neue Entwickler in die richtige Richtung führen und Ihre Faulheit in Schach halten.

pytest-archon finden Sie im Python Package Index (PyPI). Die Quellen finden Sie auf Github. Wenn Sie ein Problem finden, teilen Sie es uns bitte mit. Wenn es Ihnen gefällt, sagen Sie es weiter.

Logo Blog jw bargsten

(Sie finden diesen Beitrag auch auf dem persönlichen Blog von Joachim Bargsten)

Foto von Amiya Chaturvedi auf Unsplash

Verfasst von

Joachim Bargsten

Contact

Let’s discuss how we can support your journey.