Skip to content

8. Professionelle Entwicklung mit Python

Professionelle Python-Entwicklung besteht nicht nur aus Sprachsyntax. Entscheidend ist, wie ein Projekt eingerichtet, strukturiert, getestet, formatiert und überprüft wird.

In modernen Python-Projekten übernimmt uv viele Aufgaben, die früher auf mehrere Werkzeuge verteilt waren. Projekte werden initialisiert, Abhängigkeiten verwaltet, Entwicklungswerkzeuge installiert und reproduzierbare Umgebungen synchronisiert. Zusammen mit pytest, Ruff und Mypy entsteht daraus ein kompakter, leistungsfähiger Workflow.

Dieses Kapitel zeigt einen pragmatischen Entwicklungsprozess ohne unnötigen Werkzeugmix. Der Fokus liegt auf uv als Projektwerkzeug, Ruff für Linting und Formatierung, pytest für Tests und Mypy für statische Typprüfung.

Ziel ist es, ein professionelles Python-Projekt aufzubauen, das sauber strukturiert, automatisch prüfbar und langfristig wartbar ist.

Der professionelle Python-Workflow

Moderne Entwicklungswerkzeuge im Python-Workflow

Professionelle Python-Entwicklung basiert heute auf einem klar definierten Workflow, der weit über das reine Schreiben von Code hinausgeht. Moderne Werkzeuge übernehmen Aufgaben wie Abhängigkeitsmanagement, Testausführung, statische Analyse und Formatierung. Diese Automatisierung sorgt für Konsistenz, Wiederholbarkeit und Qualität – Aspekte, die auch erfahrene Entwickler aus Java-, C#- oder C++-Umgebungen kennen, aber in Python oft anders umgesetzt werden.

Einheitlicher Workflow: Warum er wichtig ist

Ein einheitlicher Workflow stellt sicher, dass alle Teammitglieder unter identischen Bedingungen entwickeln und testen. Anders als in klassischen Build-Systemen, die oft komplexe Konfigurationsdateien und explizite Kompilationsschritte erfordern, setzt Python auf schlanke, deklarative Konfigurationen (z. B. pyproject.toml) und isolierte Umgebungen. Das minimiert „funktioniert-nur-bei-mir“-Probleme und fördert die Wartbarkeit.

Aufgaben moderner Werkzeuge im Überblick

  • Projektinitialisierung: Automatisches Erzeugen einer konsistenten Projektstruktur, die Best Practices wie das src-Layout unterstützt.
  • Abhängigkeitsmanagement: Verwaltung von Laufzeit- und Entwicklungsabhängigkeiten mit präziser Versionskontrolle und Sperrdateien.
  • Testausführung: Integration von Testframeworks wie pytest, um Tests einfach und reproduzierbar auszuführen.
  • Codequalität: Automatisiertes Linting und Formatierung (z. B. mit Ruff), um Stil- und Fehlerquellen frühzeitig zu erkennen.
  • Statische Typprüfung: Einsatz von Mypy für frühzeitige Erkennung von Typfehlern, was besonders bei dynamischer Typisierung wertvoll ist.

Beispiel: Workflow mit uv

Das Tool uv bündelt viele dieser Aufgaben und ermöglicht einen konsistenten Ablauf:

uv init my_project          # Projektstruktur erzeugen
uv add requests              # Laufzeitabhängigkeit hinzufügen
uv add --dev pytest mypy ruff   # Entwicklungsabhängigkeiten hinzufügen
uv run pytest                # Tests ausführen
uvx ruff                     # Linting und Formatierung
uv run mypy                  # Statische Typprüfung

Dieser Workflow stellt sicher, dass alle Schritte innerhalb derselben isolierten Umgebung ablaufen, was Versionskonflikte vermeidet und reproduzierbare Ergebnisse liefert.

Best Practice: Trennung von Laufzeit- und Entwicklungsabhängigkeiten

In Python-Projekten ist es üblich, zwischen Abhängigkeiten für den Produktivcode und Tools für Entwicklung und Testing zu unterscheiden. Das verhindert, dass z. B. Testframeworks in der Produktionsumgebung installiert werden müssen. Werkzeuge wie uv unterstützen diese Trennung nativ.

Ein Projekt mit uv init erstellen

Ein neues Python-Projekt mit uv init starten

Mit uv init erzeugen Sie eine moderne Python-Projektstruktur, die bewährte Konventionen und Best Practices berücksichtigt und zwei etablierte Layouts unterstützt: das Flat-Layout und das src-Layout.

Flat-Layout vs. src-Layout

  • Flat-Layout:
  • Der Quellcode liegt direkt im Projektstammverzeichnis in einem Paketordner, z.B. my_project/.
  • Vorteile: Einfachheit, weniger Verzeichnistiefe.
  • Nachteile: Gefahr von Namenskonflikten mit anderen Dateien im Projekt, weniger klare Trennung von Quellcode und anderen Ressourcen.

my_project/
├──my_project/
   ├── main.py
   ├── api.py
   ├── models.py
   ├── services.py
   └── utils.py
├── tests/
├── pyproject.toml
├── README.md
└── .gitignore
Gestartet wird main mit:

uv run -m my_project.main
  • src-Layout:
  • Der Quellcode liegt unter src/ (z.B. src/my_project/).
  • Vorteile: Klare Trennung von Quellcode und anderen Dateien (Tests, Konfiguration, Dokumentation).
  • Verhindert versehentliches Importieren von Dateien außerhalb des Quellcodes.
  • Wird in größeren Projekten oder Bibliotheken empfohlen.

my_project/
├── src/
   └── my_project/
       ├── main.py
       ├── api.py
       ├── models.py
       ├── services.py
       └── utils.py
├── tests/
├── pyproject.toml
├── README.md
└── .gitignore
Gestartet wird main mit:

uv run -m my_project.main
Das funktioniert, weil das Projekt lokal installiert wurde von uv.

uv init fragt bei der Projekterstellung nach dem bevorzugten Layout und generiert die entsprechende Struktur.

Beispiel: Projektinitialisierung mit uv init my_project --package

uv init my_project --package
Nach Abschluss finden Sie eine Struktur ähnlich dieser vor:

my_project/
├── pyproject.toml
├── README.md
├── src/              # Nur bei src-Layout (mit der --package oder --lib Option bei uv) 
│   └── my_project/
│       └── __init__.py

Warum diese Trennung?

Das src-Layout verhindert, dass Tests oder andere Dateien versehentlich als Module importiert werden, was in großen Projekten häufig zu subtilen Fehlern führt.

Best Practice: src-Layout bevorzugen

Für professionelle Projekte, vor allem später via pip installiebare, empfiehlt sich das src-Layout, da es:

  • die Trennung von Quellcode und anderen Ressourcen sicherstellt
  • die Testumgebung sauber hält
  • den Importmechanismus klar definiert

Beispiel für eine einfache Paketstruktur im einfachen Flat-Layout

my_project/
├── __init__.py
├── api.py
└── utils.py
└──pyproject.toml

Hier liegt alles im Projektstamm, was für kleine Projekte oder Skripte ausreichend sein kann. Allerdings können bei größeren Projekten schnell Importkonflikte auftreten, wenn z.B. eine Datei utils.py existiert und gleichzeitig ein Paket utils importiert werden soll.

In api.py könnten Sie z.B. eine REST-API-Client-Klasse definieren:

class ApiClient:
    def __init__(self, base_url: str) -> None:
        self.base_url = base_url

    def fetch_data(self, endpoint: str) -> dict:
        # Beispielhafte Methode, die Daten von einer API holt
        pass

Diese Struktur lässt sich problemlos mit uv run und anderen Werkzeugen nutzen, die auf der Projektstruktur aufbauen.


Zusammenfassung

  • uv init erzeugt ein neues Python-Projekt mit moderner Struktur.
  • Zwei Layouts stehen zur Wahl: Flat (einfach) und src (empfohlen für professionelle Projekte).
  • Das src-Layout trennt Quellcode klar von anderen Dateien und verhindert Importprobleme.
  • Die Wahl der Projektstruktur ist ein wichtiger Schritt, der spätere Wartbarkeit und Tool-Integration erleichtert.

Abhängigkeiten mit uv add verwalten

Grundprinzipien der Abhängigkeitsverwaltung mit uv

In Python-Projekten ist die Verwaltung von Abhängigkeiten zentral für Reproduzierbarkeit, Wartbarkeit und Teamarbeit. Das Werkzeug uv integriert Paketmanagement nahtlos in den Workflow und nutzt dabei das standardisierte pyproject.toml als zentrale Konfigurationsdatei.

uv add ist der Befehl, um Abhängigkeiten hinzuzufügen oder zu entfernen. Dabei unterscheidet man zwischen Laufzeitabhängigkeiten (dependencies) und Entwicklungsabhängigkeiten (dev-dependencies).

Laufzeitabhängigkeiten hinzufügen

Laufzeitabhängigkeiten sind Bibliotheken, die für den Betrieb der Anwendung notwendig sind. Beispiel: Ein Web-API-Projekt benötigt fastapi und httpx.

uv add fastapi httpx

Dies aktualisiert automatisch die Sektion [dependencies] in pyproject.toml und sorgt dafür, dass die Abhängigkeiten in der virtuellen Umgebung installiert werden.

Entwicklungsabhängigkeiten verwalten

Entwicklungsabhängigkeiten sind Tools, die nur während Entwicklung und Test benötigt werden, z.B. pytest oder mypy.

uv add --dev pytest mypy

Diese landen in der Sektion [dependency-groups] und werden nicht mit in die Produktionsumgebung übernommen. Das entspricht in Java etwa dem test-Scope in Maven.

Entfernen von Abhängigkeiten

Um eine Abhängigkeit zu entfernen, nutzt man:

uv remove httpx
uv remove --dev mypy

Dies entfernt die Bibliothek sowohl aus der pyproject.toml als auch aus der virtuellen Umgebung.

Synchronisation der Abhängigkeiten

uv nutzt eine Lock-Datei (uv.lock), um die exakten Versionen der installierten Pakete festzuhalten. Das sorgt für deterministische Builds und verhindert "funktioniert-nur-bei-mir"-Probleme.

Nach Änderungen an pyproject.toml oder wenn man ein Projekt aus einem Versionskontrollsystem auscheckt, synchronisiert man die Umgebung mit:

uv sync

Dieser Befehl installiert alle Abhängigkeiten exakt in den Versionen, die in uv.lock definiert sind.

Best Practices

  • Trennung von Laufzeit- und Entwicklungsabhängigkeiten: Vermeidet unnötige Pakete in der Produktion und reduziert Sicherheitsrisiken.
  • Lock-Datei nutzen: Immer uv.lock versionieren, um reproduzierbare Builds sicherzustellen.
  • Explizite Versionen bevorzugen: Nutze Versionseinschränkungen (z.B. fastapi>=0.95,<1.0), um unerwartete Updates zu vermeiden.

Beispiel: Schritt-für-Schritt

  1. Neues Projekt initialisieren:
uv init myproject
cd myproject
  1. Laufzeitabhängigkeiten hinzufügen:
uv add requests
  1. Entwicklungsabhängigkeiten hinzufügen:
uv add --dev pytest mypy
  1. Synchronisieren (z.B. nach Auschecken aus Git):
uv sync
  1. Entfernen einer Abhängigkeit:
uv remove requests

Fazit

uv add und die zugehörigen Kommandos bieten eine klare, deklarative Schnittstelle für die Abhängigkeitsverwaltung in Python. Sie fördern eine saubere Trennung von Laufzeit- und Entwicklungsabhängigkeiten, ermöglichen reproduzierbare Builds durch Lock-Dateien und integrieren sich nahtlos in moderne Python-Workflows.

Programme mit uv run starten

uv run: Programme und Tools konsistent ausführen

Der Befehl uv run ist das zentrale Werkzeug, um Python-Code innerhalb der isolierten Projektumgebung auszuführen. Im Gegensatz zu globalen Python-Installationen sorgt uv run dafür, dass alle Abhängigkeiten, Umgebungsvariablen und Konfigurationen exakt wie im Projekt definiert geladen werden. Dies verhindert Versionskonflikte und unvorhersehbares Verhalten, das in anderen Sprachen oft durch unterschiedliche globale Umgebungen entsteht.

Warum nicht einfach python oder pytest direkt aufrufen?

Python-Projekte profitieren stark von virtuellen Umgebungen, da die Sprache selbst keine zentrale Paketverwaltung erzwingt. uv run kapselt den Aufruf von Python-Interpreter, Test-Frameworks oder anderen Tools so, dass immer die projektdefinierte Umgebung genutzt wird. Dadurch entfällt das manuelle Aktivieren von virtuellen Umgebungen oder das Risiko, globale Pakete zu verwenden.

Grundlegende Nutzung

Um ein Skript auszuführen, verwenden Sie:

uv run python main.py 

Hierbei startet uv run den Python-Interpreter mit der im Projekt definierten Umgebung.

Sie können ein bestimmtes Modul direkt ausführen:

uv run -m my_project.main
So kann die Projektstruktur auch mit src-Layout problemlos genutzt werden, da uv run den PYTHONPATH entsprechend setzt.

Tests ausführen

Tests werden mit pytest typischerweise so gestartet:

uv run pytest tests/
uv run -m pytest tests

Dies garantiert, dass die Version von pytest verwendet wird, die im Projekt als Entwicklungsabhängigkeit definiert ist.

Werkzeuge und CLI-Tools

Auch Code-Formatierer, Linter oder statische Analysewerkzeuge werden über uv run ausgeführt, z.B.:

uv run ruff check .

Dadurch bleibt die Toolchain reproduzierbar und unabhängig von globalen Installationen.

Best Practice: Konsistente Projektumgebung

  • Verwenden Sie immer uv run, um sicherzustellen, dass der korrekte Interpreter und die korrekten Abhängigkeiten geladen werden.
  • Vermeiden Sie globale Python- oder Tool-Installationen, um Versionskonflikte zu minimieren.
  • Automatisieren Sie häufige Befehle in Makefiles oder Shell-Skripten, die uv run nutzen.

Tests mit pytest schreiben

Grundprinzipien von pytest

Pytest ist das bevorzugte Testframework in der Python-Welt, da es durch seine einfache Syntax und mächtige Features überzeugt. Anders als in Java oder C#, wo Testmethoden oft durch spezielle Annotationen markiert werden, erkennt pytest automatisch alle Funktionen und Methoden, die mit test_ beginnen. Dies fördert eine minimalistische, aber ausdrucksstarke Testdefinition.

Ein typischer pytest-Test ist eine einfache Funktion, die Assertions verwendet, um Verhalten zu prüfen:

# test_calculator.py

def test_add() -> None:
    result = 1 + 2
    assert result == 3

Die Verwendung von assert ist idiomatisch und unterscheidet sich von JUnit oder NUnit, wo spezielle Assert-Methoden genutzt werden. Pytest wertet assert-Ausdrücke aus und gibt bei Fehlern detaillierte Informationen, was das Debuggen erleichtert.

Strukturierung von Tests

Tests sollten in einem eigenen Verzeichnis, z.B. tests/, liegen. Innerhalb dieses Verzeichnisses können Module parallel zur Produktivcode-Struktur angelegt werden. So bleibt die Teststruktur übersichtlich und nachvollziehbar.

your_project/
├── src/
│   └── your_project/
│       ├── __init__.py
│       ├── calculator.py
│       └── cli.py
├── tests/
│   ├── test_calculator.py
│   └── test_cli.py
├── pyproject.toml
├── README.md

Diese Trennung unterstützt den Workflow mit uv run pytest, da so die Testumgebung klar definiert ist und Importprobleme vermieden werden.

Testausführung mit uv

Mit uv run pytest wird pytest innerhalb der isolierten Projektumgebung ausgeführt. Das vermeidet globale Abhängigkeiten und sorgt für reproduzierbare Testergebnisse.

uv run pytest

Standardmäßig sucht pytest rekursiv nach Tests im Projektverzeichnis. Für gezielte Ausführung können Pfade oder Testnamen angegeben werden:

uv run pytest tests/test_calculator.py::test_add

Mehr zum Thema pytest und Testorganisation findet sich in Kapitel 13 Automatisierte Tests mit pytest.

Linting und Formatierung mit uvx ruff

Ruff ohne globale Installation nutzen

Ruff ist ein moderner Linter und Formatter, der in Rust implementiert ist und eine beeindruckende Performance bietet. Anders als in vielen Java- oder C#-Workflows, wo Linter oft global installiert und konfiguriert werden, empfiehlt sich in Python die lokale Nutzung innerhalb der Projektumgebung. Das vermeidet Versionskonflikte und sorgt für reproduzierbare Builds.

Mit uvx, dem Kommandozeilenwerkzeug aus dem uv-Ökosystem, lässt sich Ruff bequem ohne globale Installation ausführen:

uvx ruff check 

Hierbei sorgt uvx dafür, dass die richtige Ruff-Version aus den Abhängigkeiten geladen wird. Das entspricht dem Prinzip von "lokalen Tools" in Node.js oder .NET, das in Python zunehmend Standard wird.

Um tools wie mypy oder ruff direkt auszuführen, ohne uv run zu verwenden, bietet uvx eine praktische Abkürzung. Ein vorheriges Installieren in einer dev-dependencies-Gruppe ist dabei NICHT notwendig.

Konfiguration zentral in pyproject.toml

Die Konfiguration von Ruff erfolgt zentral in der pyproject.toml. Das ist ein großer Vorteil gegenüber klassischen Java- oder C#-Projekten, die oft auf mehrere Konfigurationsdateien verteilt sind. Ein Beispiel:

[tool.ruff]
line-length = 88
select = ["E", "F", "W", "C90"]
ignore = ["E203", "W503"]
exclude = ["tests/data"]
  • line-length definiert die maximale Zeilenlänge, ähnlich wie in vielen Formatter-Tools.
  • select bestimmt die zu aktivierenden Regelkategorien.
  • ignore blendet spezifische Regeln aus, z.B. um Konflikte mit Black zu vermeiden.
  • exclude schließt bestimmte Verzeichnisse aus der Prüfung aus.

Diese zentrale Steuerung fördert Konsistenz und erleichtert die Zusammenarbeit im Team.

Automatisierte Formatierung und Linting im Workflow

Ruff kann nicht nur Fehler anzeigen, sondern auch automatisch formatieren:

uvx ruff check --fix

Integration in den Entwicklungsprozess

In professionellen Python-Projekten empfiehlt es sich, Ruff als festen Bestandteil des CI/CD-Prozesses zu integrieren. Ein typischer Workflow:

  1. Entwickler führen uvx ruff check lokal vor dem Commit aus oder in einem pre-commit-Hook.
  2. CI-Server prüft den Code erneut und blockiert Builds bei Linting-Fehlern.
  3. Optional: Automatische Formatierung vor dem Merge.

Dadurch wird verhindert, dass stilistische oder einfache Fehler in den Hauptzweig gelangen.

Best Practices

  • Nutze pyproject.toml als zentrale Konfigurationsquelle für Ruff und andere Tools.
  • Führe Linting und Formatierung regelmäßig lokal und im CI aus.
  • Vermeide globale Tool-Installationen, um Versionskonflikte zu verhindern.
  • Verwende uvx ruff für konsistente Ausführung und einfache Integration.

Statische Typprüfung mit Mypy

Warum statische Typprüfung in Python?

Python ist eine dynamisch typisierte Sprache, was Flexibilität und schnelle Entwicklung ermöglicht. Gerade für erfahrene Entwickler aus statisch typisierten Sprachen wie Java oder C# stellt sich jedoch oft die Frage: Wie kann man in Python Typfehler frühzeitig erkennen, ohne die Dynamik zu verlieren?

Hier kommt Mypy ins Spiel. Mypy ist ein statischer Typprüfer, der auf den optionalen Type Hints in Python basiert. Er überprüft den Code ohne Ausführung und hilft, Fehler frühzeitig zu finden, die sonst erst zur Laufzeit auftreten würden.

Type Hints idiomatisch einsetzen

Python bietet Type Hints seit PEP 484 an. Anders als in Java oder C# sind sie optional und beeinflussen die Laufzeit nicht. Das bedeutet, dass sie primär der Lesbarkeit, Dokumentation und Werkzeugunterstützung dienen.

Beispiel einer Funktion mit Type Hints:

from typing import Optional

def fetch_user(user_id: int) -> Optional[dict]:
    # Gibt ein Benutzer-Dictionary zurück oder None, wenn nicht gefunden
    ...

Wichtig ist, dass Type Hints in Python eher als Vertrag und Dokumentation verstanden werden, die von Tools wie Mypy überprüft werden.

Mypy konfigurieren und ausführen

Mypy liest die Typinformationen aus dem Code und prüft sie gegen die tatsächliche Verwendung. Für ein modernes Python-Projekt empfiehlt sich die zentrale Konfiguration in pyproject.toml oder früher auch in einer mypy.ini:

[tool.mypy]
python_version = "3.13"
disallow_untyped_defs = true  # Keine untypisierten Funktionen
ignore_missing_imports = true  # Für externe Libraries
strict_optional = true  # Optional-Typen streng prüfen

Ein typischer Mypy-Check wird über den Befehl uv run mypy ausgeführt, wenn Mypy als Entwicklungsabhängigkeit installiert ist.

uv run mypy .

oder kürzer mit uvx:

uvx mypy .

Typfehler frühzeitig erkennen

Mypy erkennt typische Fehler, z.B. falsche Rückgabetypen, fehlende Argumente oder falsche Typen in Collections:

def process_items(items: list[int]) -> int:
    return sum(items)

result = process_items([1, 2, '3'])  # Fehler: '3' ist kein int

Dies verhindert Laufzeitfehler, die in dynamischen Tests oft schwer zu reproduzieren sind.

Schrittweise strengere Typprüfung

Gerade bei großen Codebasen empfiehlt sich ein schrittweises Vorgehen:

  1. disallow_untyped_defs = false starten, um Typprüfung nur bei vorhandenen Hints zu aktivieren.
  2. Nach und nach Funktionen typisieren und disallow_untyped_defs auf true setzen.
  3. strict_optional und weitere strenge Optionen aktivieren, um Null-Sicherheiten zu erhöhen.

So lässt sich der Aufwand kontrolliert verteilen.

Best Practices

  • Verwenden Sie Type Hints konsequent, vor allem in öffentlichen APIs.
  • Nutzen Sie Mypy oder Pyright in der IDE für sofortiges Feedback.
  • Nutzen Sie Mypy als Teil des CI-Prozesses.
  • Arbeiten Sie mit pre-commit Hooks, um Typprüfungen vor dem Commit sicherzustellen.
  • Pflegen Sie eine zentrale Konfiguration, um Konsistenz sicherzustellen.
  • Kombinieren Sie Typprüfung mit Unit Tests für maximale Sicherheit.

Clean Code in Python

Pythonische Lesbarkeit und Einfachheit

In Python steht Lesbarkeit an erster Stelle. Anders als in Java oder C# ist explizite Typisierung nicht zwingend.

class User:
    def __init__(self, name: str):
        self.name = name

    @property
    def display_name(self) -> str:
        return self.name.title()

Hier ersetzt @property die klassische Getter-Methode und hält den Zugriff einfach und intuitiv.

Vermeidung unnötiger Abstraktionen

Java- oder C++-Entwickler neigen dazu, viele Interfaces oder abstrakte Klassen zu definieren. In Python ist das oft überflüssig, da Duck Typing und dynamische Typisierung Flexibilität schaffen. Statt komplexer Vererbungsbäume sind einfache Funktionen oder Klassen mit klar definierten Schnittstellen idiomatisch.

def process(data: list[int]) -> list[int]:
    return [x * 2 for x in data]

Das ist oft besser als eine abstrakte Basisklasse mit vielen Methoden.

Klare und aussagekräftige Namen

Python legt großen Wert auf aussagekräftige, aber nicht übermäßig lange Namen. Anders als in manchen Java-Konventionen, wo oft Präfixe oder Suffixe verwendet werden, bevorzugt Python Klarheit durch Kontext und einfache Namen.

# Gut und Snake Case
def calculate_tax(amount: float) -> float:
    return amount * 0.19

# Weniger idiomatisch (und kein Snake Case)
def calcTaxAmount(amountValue: float) -> float:
    return amountValue * 0.19

Funktionen als First-Class Citizens nutzen

Python fördert funktionale Programmierungselemente. Funktionen können einfach als Argumente übergeben oder zurückgegeben werden, was in Java oder C# oft umständlicher ist.

from typing import Callable

def apply_operation(data: list[int], op: Callable) -> list[int]:
    return [op(x) for x in data]

result = apply_operation([1, 2, 3], lambda x: x + 1)

Vermeidung von unnötiger Sichtbarkeitskontrolle

In Java oder C# sind private und protected essenziell. Python folgt hier einer anderen Philosophie: "We are all consenting adults". Ein einfacher Unterstrich _ signalisiert eine interne API, aber es gibt keine echten Zugriffsmodifikatoren.

class Service:

    def _helper(self) -> None:
        """interne Methode, aber nicht wirklich privat."""
        pass

    def transform(self, data) -> list[int]:
        """öffentliche Methode, die die interne Methode nutzt."""
        self._helper()  # Aufruf der internen Methode
        return [x * 2 for x in data]

service = Service()
service._helper()  # Technisch möglich, aber signalisiert "nicht für den externen Gebrauch"
service.transform([1, 2, 3])  # Korrekte Nutzung der öffentlichen Methode

Idiomatische Fehlerbehandlung

Python verwendet Exceptions, aber der Umgang ist oft lockerer als in stark typisierten Sprachen. Es ist üblich, Fehler dort zu behandeln, wo sie sinnvoll sind, und nicht übermäßig viele Checked Exceptions zu deklarieren.

try:
    result = some_operation()
except ValueError:
    handle_value_error()

Fazit

Clean Code in Python bedeutet, sich auf die Sprache und ihre Philosophie einzulassen. Statt gewohnte Muster aus Java oder C# 1:1 zu übertragen, sollte man die Stärken von Python nutzen: klare, prägnante Syntax, dynamische Typisierung, flexible Funktionen und eine pragmatische Fehlerbehandlung.

So entsteht wartbarer, lesbarer und idiomatischer Code, der sich von klassischen objektorientierten Paradigmen löst, ohne an Professionalität einzubüßen.

Praxisprojekt: Ein kleine API

Projektinitialisierung und Struktur

Wir starten mit einem neuen Projekt, das wir mit uv init anlegen. Dabei wählen wir das src-Layout, da es eine klare Trennung zwischen Quellcode und Tests/Dokumentation ermöglicht und Importprobleme vermeidet – ein häufig unterschätztes Problem, das in Java/C# meist durch IDEs und Build-Tools abstrahiert wird.

uv init app --package
cd app

Die generierte Struktur sieht typischerweise so aus:

app/
├── src/
│   └── app/
│       └── __init__.py
├── pyproject.toml
└── README.md
└── .gitignore 

Das src-Verzeichnis schützt vor versehentlichen Importen aus dem Arbeitsverzeichnis und erzwingt saubere Paketgrenzen.

Abhängigkeiten verwalten

Wir fügen Laufzeit- und Entwicklungsabhängigkeiten mit uv add hinzu. Entwicklungsabhängigkeiten wie pytest, ruff und mypy kommen in den [tool.uv.dev-dependencies]-Block in der pyproject.toml, was klar signalisiert, dass sie nicht im Produktivcode benötigt werden.

uv add requests
uv add --dev pytest ruff mypy

Das Synchronisieren der Abhängigkeiten mit uv sync stellt sicher, dass venv und Lock-Datei konsistent sind.

Code ausführen und testen

Mit uv run führen wir Code und Tests innerhalb der isolierten Umgebung aus:

uv run -m app.main
uv run -m pytest

Linting und Formatierung

uvx ruff check .
uvx ruff check . --fix

Statische Typprüfung

uvx mypy .

DAs API-Modul

# src/app/api.py
from typing import TypedDict
import requests


class User(TypedDict):
    id: int
    name: str
    email: str


def fetch_user(user_id: int) -> User | None:
    response = requests.get(
        f"https://jsonplaceholder.typicode.com/users/{user_id}"
    )
    if response.status_code != 200:
        return None

    data = response.json()
    return User(
        id=data["id"],
        name=data["name"],
        email=data["email"],
    )
# tests/test_api.py
from app.api import fetch_user


def test_fetch_user_returns_user() -> None:
    user = fetch_user(1)

    assert user is not None
    assert user["id"] == 1
    assert isinstance(user["email"], str)

Auf Mocks oder Stubs verzichten wir hier bewusst, um den Fokus auf die Integration von Werkzeugen und den Workflow zu legen. In einem echten Projekt würden wir natürlich externe API-Aufrufe mocken, um Tests stabil und unabhängig von externen Diensten zu machen.

Zusammenführung der Werkzeuge

  • uv init legt das Projekt mit sauberer Struktur an.
  • uv add verwaltet Abhängigkeiten präzise.
  • uv run und uvx führen Code, Tests und Werkzeuge isoliert aus.
  • pytest sichert funktionale Korrektheit.
  • ruff sorgt für konsistenten Stil und vermeidet Fehler.
  • mypy erhöht die Codequalität durch Typprüfung.

Diese Kombination entspricht einem modernen, professionellen Workflow, der in Python die Balance zwischen Flexibilität und Sicherheit wahrt.