Skip to content

4. Module, Pakete und Projektstruktur

Python-Projekte professionell organisieren und moderne Werkzeuge für Paketverwaltung und Entwicklungsumgebungen einsetzen.

Während kleine Python-Skripte oft aus einer einzigen Datei bestehen, wachsen reale Anwendungen schnell zu größeren Projekten heran. Spätestens dann werden Fragen nach Projektstruktur, Wiederverwendung, Abhängigkeiten und Wartbarkeit wichtig.

Python bietet mit Modulen und Paketen ein einfaches, aber äußerst flexibles System zur Organisation von Quellcode. Gleichzeitig hat sich in den vergangenen Jahren das Ökosystem rund um Paketverwaltung und Projektkonfiguration erheblich weiterentwickelt.

Moderne Python-Projekte setzen zunehmend auf pyproject.toml als zentrale Konfigurationsdatei und verwenden Werkzeuge wie uv zur Verwaltung von Abhängigkeiten und virtuellen Umgebungen. Dennoch ist es wichtig, auch die klassischen Werkzeuge wie pip und venv zu verstehen, da sie in vielen bestehenden Projekten weiterhin anzutreffen sind.

In diesem Kapitel lernen Sie, wie professionelle Python-Projekte aufgebaut werden und welche Werkzeuge sich in der Praxis bewährt haben.

Module

Python-Module: Struktur und Organisation

In Python ist ein Modul eine einzelne Datei mit der Endung .py, die Python-Code enthält. Module sind das primäre Mittel, um Code zu strukturieren und Wiederverwendbarkeit zu schaffen. Anders als in Java oder C#, wo Klassen oft in eigenen Dateien liegen, ist in Python ein Modul flexibler: Es kann Funktionen, Klassen, Variablen und ausführbaren Code enthalten.

Modul-Docstrings und Lesbarkeit

Jedes Modul sollte mit einem Docstring beginnen, der kurz den Zweck und die enthaltenen Funktionalitäten beschreibt. Dies fördert die Lesbarkeit und erleichtert Tools wie help() oder Dokumentationsgeneratoren.

"""
Modul zur Verarbeitung von API-Anfragen und Antwortvalidierung.
"""

def validate_response(response: dict) -> bool:
    """Validiert die API-Antwort auf korrekte Struktur."""
    # Implementierung
    return 'status' in response

Warum Module? Vorteile gegenüber monolithischen Skripten

  • Wartbarkeit: Kleine, fokussierte Module sind leichter zu verstehen und zu testen.
  • Wiederverwendbarkeit: Module können in verschiedenen Projekten importiert werden.
  • Namespace-Isolation: Variablen und Funktionen werden in ihrem Modul-Namespace gekapselt, was Namenskonflikte vermeidet.

Importieren von Modulen: Best Practices

Python bietet verschiedene Importvarianten:

import modulename
from modulename import function_name
from modulename import *  # wird nicht empfohlen
  • import modulename ist klar und vermeidet Namenskollisionen.
  • from modulename import function_name eignet sich, wenn nur wenige Funktionen benötigt werden.
  • from modulename import * sollte vermieden werden, da es den Namespace verschmutzt und die Lesbarkeit verschlechtert.

Typische Fehler beim Modulgebrauch

  • Ausführung von Code beim Import: Python führt beim Import den gesamten Modulcode aus. Initialisierungen oder Tests sollten daher in if __name__ == "__main__": ausgelagert werden.
  • Zirkuläre Importe: Vermeiden Sie zyklische Abhängigkeiten, da sie zu Importfehlern führen.

Vergleich zu Java und C

In Java oder C# ist die Datei-Struktur oft strikt an Klassen gebunden, während Python mehr Freiheit bietet. Diese Flexibilität erlaubt schnelle Prototypen und modulare Designs, erfordert aber Disziplin bei der Organisation.

Beispiel: Modulstruktur in einem Webprojekt

webapp/
├── __init__.py
├── api.py       # API-Endpunkte
├── auth.py      # Authentifizierung
├── db.py        # Datenbankzugriff
└── utils.py     # Hilfsfunktionen

Jedes Modul hat klar umrissene Verantwortlichkeiten, was Wartung und Testbarkeit verbessert.

Fazit

Module sind das Fundament der Python-Projektstruktur. Sie fördern eine klare Trennung von Verantwortlichkeiten und erleichtern das Teilen und Wiederverwenden von Code. Ein gut dokumentiertes Modul mit einem prägnanten Docstring und bewusster Importstrategie ist ein Zeichen professionellen Python-Codes.

Importe verstehen

Das Python-Importsystem: Grundlagen und Philosophie

Python-Module und deren Importe sind das Rückgrat jeder größeren Anwendung. Ein Import lädt ein Modul nur einmal pro Interpreter-Session, was Performancevorteile bringt, aber auch Seiteneffekte beim Importieren bedeuten kann.

Absolute vs. relative Importe

Python unterscheidet zwischen absoluten und relativen Importen. Absolute Importe beziehen sich auf den vollständigen Pfad vom Projekt- oder Paket-Root aus, während relative Importe sich auf die Position des aktuellen Moduls beziehen.

# Absolute Importe
from myproject.utils import helpers

# Relative Importe (innerhalb eines Pakets)
from .helpers import some_function
from ..core import main

Warum diese Unterscheidung?

  • Absolute Importe sind klar und robust, besonders bei größeren Projekten oder wenn Module außerhalb des aktuellen Pakets importiert werden.
  • Relative Importe erhöhen die Wartbarkeit innerhalb eines Pakets, da sie weniger anfällig für Umbenennungen oberer Paketstrukturen sind.

Importvarianten und ihre Auswirkungen

  • import modulname lädt das Modul und bindet es unter modulname.
  • from modulname import symbol bindet das Symbol direkt, was den Code kürzer, aber potenziell weniger transparent macht.
import os
print(os.path.join('a', 'b'))

from os.path import join
print(join('a', 'b'))

Best Practice: Verwende import modulname für Klarheit und vermeide Namenskonflikte. Nutze from modul import symbol gezielt, wenn die Lesbarkeit dadurch steigt und keine Konflikte entstehen.

Der Importpfad und sys.path

Python sucht Module in den Verzeichnissen, die in sys.path gelistet sind. Dieses umfasst u.a.:

  • Das aktuelle Arbeitsverzeichnis
  • Standardbibliothek
  • Installierte Drittanbieter-Pakete

Verzeichnisse können zur Laufzeit manipuliert werden, was dynamische Importmechanismen ermöglicht, aber mit Vorsicht zu genießen ist.

Import-Hooks und dynamische Importe

Fortgeschrittene Anwendungen nutzen importlib für dynamische Importe oder um Module zur Laufzeit zu laden. Dies ist besonders in Plugin-Architekturen oder bei API-Clients hilfreich.

import importlib

module_name = 'myplugin'
plugin = importlib.import_module(module_name)
plugin.run()

Häufige Fehler und Stolperfallen

  • Namenskonflikte: Module im Projekt mit gleichen Namen wie Standardbibliothek oder Drittanbieter-Pakete können zu unerwartetem Verhalten führen.
  • Relative Importe außerhalb von Paketen: Relative Importe funktionieren nur innerhalb von Paketen, nicht in Skripten, die direkt ausgeführt werden.
  • Import-Zyklen: Zirkuläre Importe können zu ImportError oder AttributeError führen. Hier hilft eine klare Trennung von Verantwortlichkeiten und ggf. Lazy-Imports.

Zusammenfassung

Python-Importe sind flexibel und mächtig, erfordern aber ein Verständnis der Paketstruktur und des Laufzeitkontexts. Absolute Importe bieten Robustheit, relative Importe fördern die Paketkapselung. Explizite und klar strukturierte Importe erhöhen die Lesbarkeit und Wartbarkeit, besonders in größeren Projekten.

Im nächsten Abschnitt werden wir sehen, wie Module zu Paketen zusammengefasst werden und wie __init__.py Dateien die Paketstruktur definieren.

Pakete

Pakete als logische Gruppierung von Modulen

In Python sind Pakete eine zentrale Möglichkeit, um mehrere Module sinnvoll zu strukturieren und logisch zu gruppieren. Ein Paket ist im Kern ein Verzeichnis mit mindestens einer __init__.py-Datei, die das Verzeichnis als Paket kennzeichnet. Diese Datei kann leer sein, dient aber oft dazu, Initialisierungscode auszuführen oder die öffentliche API des Pakets zu definieren.

Beispiel einer einfachen Paketstruktur

myproject/
├── mypackage/
│   ├── __init__.py
│   ├── database.py
│   ├── api.py
│   └── utils.py
└── main.py
  • mypackage/__init__.py kann z.B. wichtige Klassen oder Funktionen importieren, um sie direkt beim Import von mypackage verfügbar zu machen.

init.py: API-Definition und Initialisierung

# mypackage/__init__.py
from .database import DatabaseConnection
from .api import fetch_data

__all__ = ['DatabaseConnection', 'fetch_data']

Dieses Muster sorgt dafür, dass from mypackage import * nur die explizit exportierten Namen lädt, was die API klar definiert und ungewollte Importe vermeidet.

Flat-Layout vs. Src-Layout

Zwei verbreitete Projektstrukturen sind:

Flat-Layout

myproject/
├── mypackage/
│   ├── __init__.py
│   └── ...
├── tests/
└── setup.py
  • Einfach und direkt, aber birgt das Risiko, dass lokale Skripte versehentlich mit Systemmodulen kollidieren.

Src-Layout

myproject/
├── src/
│   └── mypackage/
│       ├── __init__.py
│       └── ...
├── tests/
└── pyproject.toml
  • Besser geeignet für größere Projekte, da es klare Trennung zwischen Quellcode und anderen Dateien schafft und Importkonflikte vermeidet.

Importverhalten in Paketen

Innerhalb von Paketen sind relative Importe üblich und empfohlen, um Abhängigkeiten klar zu halten und Refaktorisierungen zu erleichtern:

# mypackage/api.py
from .database import DatabaseConnection

Absolute Importe sind ebenfalls möglich, werden aber bei komplexen Paketen oft unübersichtlich.

Best Practices

  • Definieren Sie in __init__.py nur die öffentliche API, keine Implementierungsdetails.
  • Nutzen Sie relative Importe innerhalb von Paketen, um Kopplung zu reduzieren.
  • Verwenden Sie das Src-Layout für größere Projekte, um Namenskonflikte zu vermeiden.
  • Vermeiden Sie zu tiefe Paketstrukturen, da Python-Importe sonst unübersichtlich werden.

Zusammenfassung

Pakete sind in Python das Mittel der Wahl, um Module logisch zu gruppieren und eine saubere API zu definieren. Die __init__.py-Datei ist dabei mehr als nur ein Marker: Sie ermöglicht Initialisierung und API-Definition. Die Wahl der Projektstruktur (Flat vs. Src) beeinflusst die Wartbarkeit und Importklarheit maßgeblich. Für erfahrene Entwickler ist das Verständnis dieser Konzepte essenziell, um Python-Projekte professionell zu organisieren und langfristig wartbar zu gestalten.

Virtuelle Umgebungen

Warum virtuelle Umgebungen in Python?

In Python ist die Verwaltung von Abhängigkeiten und Laufzeitumgebungen komplexer als in vielen kompilierenden Sprachen wie Java oder C#. Dort sind Abhängigkeiten meist zentral im Build-Tool (z.B. Maven, MSBuild) definiert und Versionierung erfolgt projektübergreifend. Python-Pakete werden jedoch global oder benutzerspezifisch installiert, was zu Versionskonflikten führt („Dependency Hell“).

Eine virtuelle Umgebung isoliert Projektabhängigkeiten vollständig vom globalen Python-Interpreter und anderen Projekten. So können unterschiedliche Projekte unterschiedliche Versionen derselben Bibliothek nutzen, ohne sich gegenseitig zu beeinflussen. Dies ist essenziell für reproduzierbare Builds, sauberes Deployment und parallele Entwicklung.

Das Modul venv

Python bringt seit Version 3.3 das Modul venv mit, um virtuelle Umgebungen zu erstellen und zu verwalten. Im Gegensatz zu externen Tools wie virtualenv ist venv Teil der Standardbibliothek und genügt für die meisten Anwendungsfälle.

Erstellen einer virtuellen Umgebung

python3 -m venv env
Dies legt im aktuellen Verzeichnis einen Ordner env an, der eine isolierte Python-Umgebung enthält.

Aktivieren der Umgebung

  • Auf Unix/macOS:
    source env/bin/activate
    
  • Auf Windows (PowerShell):
    .\env\Scripts\Activate.ps1
    

Nach Aktivierung ist der Python-Interpreter und pip auf diese Umgebung beschränkt.

Deaktivieren

deactivate

Typische Struktur eines Projekts mit venv

my_project/
├── env/                # virtuelle Umgebung (meist im .gitignore)
├── src/                # Quellcode
├── tests/              # Tests
├── requirements.txt    # Abhängigkeiten
└── README.md

Warum nicht globale Installation?

  • Versionskonflikte vermeiden: Zwei Projekte benötigen unterschiedliche Versionen von requests.
  • Reproduzierbarkeit: CI/CD-Pipelines nutzen dieselben Abhängigkeiten wie lokal.
  • Sauberkeit: Globale Installation bleibt unverändert, System-Python bleibt stabil.

Vergleich zu Java/C#

In Java oder C# sind Abhängigkeiten typischerweise im Projekt-Manifest (pom.xml, csproj) definiert und werden beim Build automatisch heruntergeladen. Die Laufzeitumgebung ist meist ein global installierter JRE/JDK oder .NET Runtime, die keine Isolation auf Bibliotheksebene bietet. Python trennt Laufzeit (Interpreter) und Bibliotheken stärker, was virtuelle Umgebungen notwendig macht.

Best Practices

  • Lege virtuelle Umgebungen außerhalb des Quellcodes oder ignoriere sie im Versionskontrollsystem.
  • Nutze requirements.txt oder moderne Tools wie uv (siehe späteres Kapitel) zur Verwaltung von Abhängigkeiten.
  • Aktiviere die virtuelle Umgebung immer vor der Entwicklung oder dem Ausführen von Skripten.

Beispiel: Installation und Nutzung einer Bibliothek

python3 -m venv env
source env/bin/activate
pip install requests
python
>>> import requests
>>> response = requests.get('https://api.example.com/data')
>>> print(response.status_code)
200

Fazit

Virtuelle Umgebungen sind in Python unverzichtbar, um saubere, reproduzierbare und konfliktfreie Entwicklungsumgebungen zu schaffen. Das in der Standardbibliothek enthaltene venv bietet eine einfache und robuste Lösung, die sich nahtlos in den Python-Workflow integriert.

Paketverwaltung mit pip

Pakete installieren und verwalten mit pip

pip ist das zentrale Werkzeug zur Paketverwaltung in Python und vergleichbar mit NuGet in C# oder Maven/Gradle in Java, jedoch mit einem Fokus auf Einfachheit und Flexibilität. Es ermöglicht das Installieren, Aktualisieren und Entfernen von Paketen aus dem Python Package Index (PyPI) oder anderen Quellen.

Installation und Upgrade von Paketen

Pakete werden typischerweise projektbezogen in virtuellen Umgebungen installiert, um Versionskonflikte zu vermeiden. Die grundlegenden Befehle lauten:

pip install requests==2.28.1  # Exakte Version installieren
pip install --upgrade requests  # Paket aktualisieren
pip uninstall requests  # Paket entfernen

Die Versionsspezifikation ist wichtig, um reproduzierbare Umgebungen zu gewährleisten. Anders als in vielen statisch typisierten Sprachen, wo Abhängigkeiten oft zentral und strikt im Build-Tool definiert sind, ist Python hier flexibler, was aber auch zu "Dependency Hell" führen kann, wenn nicht sorgfältig verwaltet.

Anforderungen dokumentieren: requirements.txt

Für reproduzierbare Builds und Teamarbeit ist es üblich, alle Abhängigkeiten in einer requirements.txt-Datei zu dokumentieren:

requests==2.28.1
flask>=2.2,<3.0

Diese Datei kann mit

pip install -r requirements.txt

eingelesen werden. So wird sichergestellt, dass alle Entwickler und Deployment-Umgebungen dieselben Paketversionen verwenden.

Best Practices bei requirements-Dateien

  • Exakte Versionen (==) für Produktion: Minimiert Überraschungen durch API-Änderungen.
  • Versionseinschränkungen (~=, >=, <) für Entwicklung: Erlaubt Flexibilität bei Bugfixes.
  • Mehrere Dateien: requirements.txt für Laufzeit, requirements-dev.txt für Entwicklungswerkzeuge.

Unterschied zu Java/C

Im Vergleich zu Maven oder NuGet sind pip und requirements.txt weniger formal und bieten keine automatische Transitivitätsauflösung mit Sperrdateien (Lockfiles) im Standard. Werkzeuge wie uv (siehe späteres Kapitel) oder Poetry ergänzen diese Lücke.

Pip und virtuelle Umgebungen

pip installiert Pakete standardmäßig in die aktive Python-Umgebung. Um globale Konflikte zu vermeiden, sollten virtuelle Umgebungen (z.B. mit venv) genutzt werden. Innerhalb einer solchen Umgebung ist pip lokal und isoliert.

Beispiel: Paketinstallation und Verwaltung in einem Webprojekt

python -m venv .venv
source .venv/bin/activate  # Linux/macOS
.\.venv\Scripts\activate  # Windows
pip install flask requests
pip freeze > requirements.txt

pip freeze listet alle aktuell installierten Pakete mit exakten Versionen auf und ist die Basis für die requirements.txt.

Häufige Fehler

  • Pakete global installieren: Führt oft zu Versionskonflikten.
  • Keine Versionsspezifikation: Erschwert reproduzierbare Deployments.
  • Manuelles Editieren von requirements.txt ohne pip freeze: Kann zu Inkonsistenzen führen.

Zusammenfassung

pip ist das Rückgrat der Python-Paketverwaltung und erlaubt durch einfache Befehle eine flexible Handhabung von Abhängigkeiten. Die Kombination mit virtuellen Umgebungen und requirements.txt-Dateien ist essenziell für professionelle Projekte, um Konsistenz und Wartbarkeit sicherzustellen. Anders als in streng typisierten Ökosystemen ist Python hier pragmatisch und setzt auf Entwicklerdisziplin und ergänzende Tools wie uv für eine moderne Projektverwaltung.

Moderne Projektverwaltung mit uv

Einführung in uv

uv ist ein modernes Kommandozeilenwerkzeug zur Verwaltung von Python-Projekten, das virtuelle Umgebungen, Abhängigkeiten und Projektstrukturen nahtlos integriert. Im Gegensatz zu klassischen Tools wie venv oder pip kombiniert uv die Funktionen von virtuellen Umgebungen und Paketmanagement in einem Workflow und ermöglicht so eine effiziente Projektverwaltung.

Projekt erstellen mit uv

Ein neues Projekt wird mit einem einzigen Befehl initialisiert:

uv init my_project

Dieser Befehl erzeugt eine standardisierte Projektstruktur inklusive einer virtuellen Umgebung und einer pyproject.toml-Datei, die als zentrale Konfigurationsdatei dient.

Typische Struktur nach uv init:

my_project/
├── src/
│   └── my_project/
│       └── __init__.py
├── tests/
├── .venv/
└── pyproject.toml

Diese Struktur trennt Quellcode (src) und Tests klar voneinander, was der Best Practice entspricht und die Wartbarkeit erhöht.

Abhängigkeiten verwalten

Abhängigkeiten werden über uv komfortabel hinzugefügt und verwaltet:

uv add requests
uv add --dev pytest

Dabei wird die virtuelle Umgebung automatisch aktiviert, und die pyproject.toml wird entsprechend angepasst. Laufzeit- und Entwicklungsabhängigkeiten werden sauber getrennt, was reproduzierbare Builds unterstützt.

Virtuelle Umgebung und Shell

uv kapselt die virtuelle Umgebung und bietet einen einfachen Zugang zur Shell:

uv shell

Damit wird eine neue Shell mit aktivierter virtueller Umgebung gestartet. Dies vermeidet das manuelle Aktivieren mit source .venv/bin/activate und ist plattformübergreifend konsistent.

Programme starten mit uv run

Anstatt Skripte direkt mit python auszuführen, empfiehlt sich der Aufruf über uv run:

uv run -m my_project.main

uv run sorgt dafür, dass die virtuelle Umgebung aktiviert ist und alle Abhängigkeiten geladen werden. Zudem können Umgebungsvariablen und Laufzeitparameter zentral verwaltet werden.

Vorteile gegenüber klassischen Workflows

  • Integrierte Verwaltung: Kein manuelles Erstellen und Aktivieren von virtuellen Umgebungen.
  • Zentrale Konfiguration: pyproject.toml als Single Source of Truth für Abhängigkeiten und Projektmetadaten.
  • Konsistenz: Einheitliche Befehle für Installation, Ausführung und Shell-Zugang.
  • Reproduzierbarkeit: Klare Trennung von Laufzeit- und Entwicklungsabhängigkeiten.

Best Practices

  • Nutze uv als zentrales Werkzeug für alle Projektmanagement-Aufgaben.
  • Verwalte Abhängigkeiten ausschließlich über uv add und uv remove.
  • Starte Programme immer über uv run, um Umgebungsprobleme zu vermeiden.

Zusammenfassung

uv modernisiert die Python-Projektverwaltung durch Integration von virtuellen Umgebungen, Abhängigkeitsmanagement und Ausführung in einem Tool. Für Entwickler mit Erfahrung in Rust oder JavaScript bietet uv eine vertraute, konsistente und reproduzierbare Art, Python-Projekte zu strukturieren und zu betreiben.

Die pyproject.toml

Einführung in die pyproject.toml

Die Datei pyproject.toml ist heute das zentrale Konfigurationsformat moderner Python-Projekte. Sie wurde mit PEP 518 eingeführt, um Build-Tools, Abhängigkeiten und Projektmetadaten in einer einzigen, klar strukturierten Datei zu vereinheitlichen. Im Vergleich zu früheren, fragmentierten Ansätzen (z.B. setup.py, requirements.txt, setup.cfg) bietet pyproject.toml eine deklarative und standardisierte Möglichkeit, Projekte zu konfigurieren.

Aufbau und zentrale Bereiche

Die pyproject.toml ist im TOML-Format geschrieben, das sich durch einfache Lesbarkeit und klare Struktur auszeichnet. Die wichtigsten Abschnitte sind:

  • [project]: Enthält Metadaten zum Projekt wie Name, Version, Autoren, Lizenz und Abhängigkeiten.
  • [build-system]: Definiert das Build-Backend und dessen Anforderungen.
  • Tool-spezifische Konfigurationen (z.B. für Linter, Formatter, Test-Frameworks) unter [tool.<name>].

Beispiel: Minimaler Projektabschnitt

[project]
name = "my_api_client"
version = "0.1.0"
description = "Asynchroner HTTP-Client für REST APIs"
authors = ["Max Mustermann <max@example.com>"]

# Laufzeitabhängigkeiten
dependencies = [
    "httpx>=0.23",
    "pydantic>=1.10"
]

[build-system]
requires = ["setuptools>=65.5", "wheel"]
build-backend = "setuptools.build_meta"

Laufzeit- vs. Entwicklungsabhängigkeiten

Im Gegensatz zu requirements.txt trennt pyproject.toml klar zwischen Laufzeit- und Entwicklungsabhängigkeiten. Laufzeitabhängigkeiten werden im [project]-Block unter dependencies gelistet. Entwicklungsabhängigkeiten (z.B. für Tests, Linting oder Dokumentation) werden typischerweise über das Tool-Management (z.B. uv) verwaltet und in eigenen Abschnitten oder Dateien gepflegt.

Diese Trennung fördert reproduzierbare Umgebungen und klare Abgrenzungen, ähnlich wie in Java oder C# mit Build- und Test-Konfigurationen.

Tool-Konfigurationen

Viele Tools lesen ihre Konfiguration direkt aus pyproject.toml. So vermeidet man separate Konfigurationsdateien und reduziert die Komplexität.

Beispiel für black und mypy:

[tool.black]
line-length = 88

[tool.mypy]
strict = true
python_version = "3.13"

Build-Backends

Der Abschnitt [build-system] definiert, wie das Projekt gebaut wird. Python unterstützt verschiedene Build-Backends, z.B. setuptools, flit oder poetry (hier nur am Rande erwähnt).

[build-system]
requires = ["setuptools>=65.5", "wheel"]
build-backend = "setuptools.build_meta"

Dieser Abschnitt ist essenziell, damit Werkzeuge wie pip das Projekt korrekt bauen und installieren können.

Vergleich zu Java/C

In Java oder C# sind Build- und Projektkonfigurationen oft in XML- oder JSON-Dateien (z.B. pom.xml bei Maven, .csproj bei .NET) organisiert. Python setzt mit pyproject.toml auf eine deklarative, leicht lesbare Syntax, die gleichzeitig flexibel genug ist, um verschiedene Tools und Workflows zu integrieren.

Best Practices

  • Versionierung: Halte version im [project]-Block aktuell und konsistent.
  • Abhängigkeiten: Nutze semantische Versionierung und setze Mindestversionen, um Kompatibilitätsprobleme zu vermeiden.
  • Tool-Konfiguration: Bündle alle Tool-Einstellungen in pyproject.toml, um verstreute Konfigurationsdateien zu vermeiden.
  • Build-Backend: Wähle ein Build-Backend, das zu deinem Workflow passt, und dokumentiere es im Projekt.

Zusammenfassung

Die pyproject.toml ist das Herzstück moderner Python-Projektorganisation. Sie vereint Metadaten, Abhängigkeiten, Build-Informationen und Tool-Konfigurationen in einer einzigen, gut strukturierten Datei. Für Entwickler mit Java- oder C#-Hintergrund bietet sie ein vergleichbares, aber flexibleres und Python-idiomatisches Konzept für Projektmanagement und Build-Prozesse.

Abhängigkeiten verwalten

Laufzeit- und Entwicklungsabhängigkeiten trennen

In Python-Projekten ist es üblich, zwischen Laufzeitabhängigkeiten (Dependencies) und Entwicklungsabhängigkeiten (Dev-Dependencies) zu unterscheiden. Laufzeitabhängigkeiten sind Bibliotheken, die Ihre Anwendung benötigt, um korrekt zu funktionieren. Entwicklungsabhängigkeiten dienen ausschließlich Werkzeugen wie Test-Frameworks, Lintern oder Build-Tools.

Diese Trennung verbessert die Wartbarkeit und vermeidet unnötige Pakete in Produktionsumgebungen – ein Konzept, das in Java oder C# oft über unterschiedliche Build-Profile (z. B. Maven Profiles, NuGet-Konfigurationen) umgesetzt wird.

Verwaltung mit uv

Das Tool uv (Universal Virtualenv) unterstützt diese Trennung elegant über die pyproject.toml. Beispiel:

[project]
dependencies = [
    "requests>=2.28",
]

[dependency-groups]
dev = [
    "pytest>=7.0",
    "black",
]

Reproduzierbare Umgebungen schaffen

Reproduzierbarkeit ist in Python-Projekten essenziell, um "funktioniert auf meinem Rechner"-Probleme zu vermeiden. Neben virtuellen Umgebungen ist die exakte Versionierung der Abhängigkeiten entscheidend.

Dazu werden häufig sogenannte Lock-Dateien verwendet, die alle transitive Abhängigkeiten mit ihren exakten Versionen festhalten. uv generiert automatisch eine uv.lock-Datei, ähnlich wie package-lock.json in JavaScript oder packages.lock.json in .NET.

uv sync
uv lock  # Sperrt alle Abhängigkeiten auf spezifische Versionen

Diese Lock-Datei sollte ins Versionskontrollsystem eingecheckt werden, um konsistente Builds zu gewährleisten.

Umgang mit requirements.txt

Obwohl pyproject.toml und Lock-Dateien moderner sind, begegnet man in vielen Projekten noch requirements.txt. Diese Datei listet Abhängigkeiten zeilenweise auf, z. B.:

requests>=2.28
pytest>=7.0

Für komplexere Projekte ist das Format jedoch weniger geeignet, da es keine Trennung zwischen Laufzeit- und Entwicklungsabhängigkeiten oder Lock-Mechanismen unterstützt.

Best Practices für Abhängigkeitsmanagement

  • Trennung von Laufzeit- und Entwicklungsabhängigkeiten: Vermeiden Sie unnötige Pakete in Produktionsumgebungen.
  • Verwenden Sie Lock-Dateien: Sorgen Sie für reproduzierbare Umgebungen.
  • Nutzen Sie uv für konsistente Verwaltung: uv integriert virtuelle Umgebungen, Installation und Locking in einem Workflow.
  • Regelmäßige Updates mit Bedacht: Aktualisieren Sie Abhängigkeiten kontrolliert, um Kompatibilitätsprobleme zu vermeiden.

Beispiel: Setup eines Web-API-Projekts

uv add fastapi uvicorn
uv add --dev pytest httpx
uv run uvicorn main:app --reload

Hierbei sind fastapi und uvicorn für den Betrieb notwendig, während pytest und httpx nur für Tests gebraucht werden.

Zusammenfassung

Abhängigkeitsmanagement in Python profitiert von klarer Trennung, reproduzierbaren Umgebungen und modernen Tools wie uv. Im Vergleich zu Java oder C# ist Python hier flexibler, aber auch anfälliger für "Dependency Hell" ohne konsequente Locking-Strategien. Die Python-Denkweise setzt auf einfache, deklarative Konfigurationen und automatisierte Umgebungsverwaltung, um Wartbarkeit und Stabilität zu gewährleisten.

Ausführbare Module und main

Ausführbare Module mit __main__

In Python ist das Konzept eines ausführbaren Moduls ein zentrales Idiom, das sich von der Main-Methode in Java oder C# unterscheidet. Statt eine spezielle Main-Methode zu definieren, nutzt Python die magische Variable __name__ und die Datei __main__.py, um den Einstiegspunkt eines Programms zu bestimmen.

if __name__ == "__main__"

Jedes Python-Modul besitzt die Variable __name__. Wird ein Modul direkt ausgeführt, erhält __name__ den Wert "__main__". Wird es hingegen importiert, entspricht __name__ dem Modulnamen.

Diese Eigenschaft wird genutzt, um Code nur beim direkten Ausführen auszuführen:

# modul.py

def main() -> None:
    print("Programm läuft direkt")

if __name__ == "__main__":
    main()

Wird modul.py importiert, passiert nichts, da der Codeblock nicht ausgeführt wird. Dies entspricht dem Schutzmechanismus in Java oder C# für den Einstiegspunkt, ist aber flexibler und ermöglicht modulare Wiederverwendung.

Ausführbare Pakete und __main__.py

Pakete können ebenfalls ausführbar sein, wenn sie eine Datei __main__.py enthalten. Diese wird ausgeführt, wenn das Paket mit python -m paketname gestartet wird.

Beispiel Projektstruktur:

projekt/
├── paket/
│   ├── __init__.py
│   ├── __main__.py
│   └── modul.py

In __main__.py befindet sich typischerweise der Programmstartcode:

# paket/__main__.py
from .modul import main

if __name__ == "__main__":
    main()

Der Aufruf erfolgt dann über:

python -m paket

Dies ist besonders nützlich, um Pakete als eigenständige Programme anzubieten und gleichzeitig ihre Module importierbar zu halten.

Vorteile gegenüber klassischen Main-Methoden

  • Modularität: Der Einstiegspunkt ist nur ein Teil des Moduls, andere Funktionen bleiben importierbar.
  • Flexibilität: Ein und dasselbe Modul kann als Skript oder Bibliothek genutzt werden.
  • Paketintegration: Pakete können als Programme gestartet werden, ohne separate Skripte zu benötigen.

Typische Fehler und Best Practices

  • Nicht __main__ prüfen: Code, der beim Import unerwartet ausgeführt wird, erschwert Tests und Wiederverwendung.
  • Komplexen Code in if __name__ == "__main__" packen: Besser ist es, die Logik in Funktionen auszulagern und nur den Aufruf in den Block zu schreiben.

Beispiel: CLI-Tool als Paket

cli_tool/
├── cli_tool/
│   ├── __init__.py
│   ├── __main__.py
│   └── commands.py
└── setup.py
# cli_tool/__main__.py
from .commands import run

if __name__ == "__main__":
    run()
# cli_tool/commands.py
import argparse

def run() -> None:
    parser = argparse.ArgumentParser(description="Beispiel CLI Tool")
    parser.add_argument('--verbose', action='store_true')
    args = parser.parse_args()
    if args.verbose:
        print("Verbose Mode aktiviert")
    else:
        print("Standardmodus")

Durch python -m cli_tool startet das Tool, ohne dass separate Skripte nötig sind.


Ausführbare Module und __main__.py sind ein eleganter Mechanismus, um Python-Programme flexibel, modular und idiomatisch zu strukturieren. Sie ermöglichen es, Pakete und Module sowohl als Bibliotheken als auch als eigenständige Anwendungen zu verwenden – ein Paradigma, das sich deutlich von dem statischen Main-Methoden-Konzept anderer Sprachen unterscheidet und die dynamische Natur von Python unterstreicht.