Im Gegensatz zu Rust’s Cargo oder Node.js npm oder auch der Java Maven/Gradle Welt wirkt das Packaging-Ökosystem in Python etwas weniger aufgeräumt. Glücklicherweise hat sich auf dem Sektor sehr viel getan in den letzten Jahren, sodass man nun mit dem Python Package- und Projektmanager uv sehr gut arbeiten kann.
Eine solide Übersicht bietet inzwischen der Python Packaging User Guide der Python Package Authority, sodass wir hier nur die wesentlichen Begrifflichkeiten klären müssen. Alles weitere lässt sich bei Bedarf dann im Packaging User Guide nachlesen, etwa wenn man selbst ein Paket anderen zur Verfügung stellen möchte.
Ebenfalls gutes Ergänzungsmaterial zu diesem Kapitel ist Peter Wang’s PyBay 2024 Vortrag, “The Five Demons of Python Packaging”, darüber, wieso Packaging in der Python-Welt besonders schwer ist.
__init__.py oder was ist ein Paket?¶
Ein Paket (package) in Python ist nichts anderes als ein Ordner, der mindestens eine (meist leere) Datei namens __init__.py enthält, und typischerweise weitere .py-Dateien. Das Zen of Python enthält die Weisheit “Namespaces are one honking great idea—let’s do more of those!” und so wird jedes Paket zu einem Namespace.
import thisDie Ordnerstruktur database/utils/misc/compare.py erlaubt in Python import database.utils.misc.compare. Es gibt auch eine Nischenanwendung, die __init__.py wegzulassen, wenn man Namespaces anlegen möchte, die über mehrere Pakete hinweg verwendet werden (namespace packages). Die Datei __init__.py darf auch Code enthalten, der ausgeführt wird, wenn das Paket importiert wird. Darin lassen sich z.B. Aliase für Unterpakete definieren um ein API bereitszustellen. Früher wurden auch Metadaten wie Version, Autoren dort hinterlegt, aber das ist abgelöst durch pyproject.toml.
Pip installs packages¶
Pip ist ein Python-Modul, welches Pakete vom Python Package Index (PyPI) herunterladen kann.
Es gibt zwar auf den meisten System ein Kommando pip aber es ist üblich, mit python -m pip zu arbeiten, damit sichergestellt ist, dass das verwendete Pip in der gleichen Umgebung arbeitet wie python (denn es können z.B. mehrere Python-Installationen vorhanden sein). Wir wollen in der Regel die aktuellste Version von Pip verwenden:
python3 -m pip install --upgrade pipMit python -m pip install mypy wird der Type Checker Mypy installiert (siehe den Abschnitt über Type Hints), und auch alle Abhängigkeiten (Pakete, die von Mypy benötigt werden).
Ein typischer Weg, die Abhängigkeiten zu dokumentieren (und insbesondere die Versionen der Pakete, die benötigt werden), ist eine requirements.txt, in der jede Zeile eine Paketversionsbeschreibung ist, wie sie auch direkt pip übergeben werden kann:
python -m pip install django==4.2.1 # Exakt diese Version
python -m pip install "django>=4.0" # Minimalversion
python -m pip install "django~=4.2.0" # Kompatibel (>= 4.2.0, == 4.2.*)Manche Pakete haben optionale Features, die man beim Installieren auswählen kann, hier etwa das Feature security des Pakets requests:
python -m pip install "requests[security]"Diese Zusatzfeatures haben in der Regel eigene Abhängigkeiten, was also z.B. auch zu Konflikten und Sicherheitsrisiken führen kann.
Insgesamt regelt PEP 508 wie Abhängigkeiten ausgedrückt werden können. Das Beispiel aus der Dokumentation, was alle Features vereint ist auch requests:
requests [security,tests] >= 2.8.1, == 2.8.* ; python_version < "2.7"Ein üblicher Ablauf ist, wenn man in Quelltexte-Ordner eines Python-Projektes python projekt.py ausführen möchte, zuvor mit
python -m pip install -r requirements.txtdie Abhängigkeiten zu installieren.
Man kann auch Pakete installieren, die nicht im PyPI gelistet sind, etwa ein Git Repository via
python -m pip install git+https://github.com/user/reponame.git@branchAuch einen lokalen Ordner oder ein Archiv, welches ein Python-Paket enthält kann man installieren, indem man den Pfad explizit angibt.
Man entfernt Pakete mit pip uninstall.
Es gibt zu jeder Python-Installation mindestens zwei Orte, an denen Pakete liegen können: der systemweite Ordner site-packages und ein entsprechender Ordner pro Nutzer. Dabei wird import das Paket immer zuerst beim Benutzer suchen, bevor es die systemweiten site-packages absucht.
Mit der inzwischen überarbeiteten PEP 668 angefangen sollte der Konflikt zwischen Pip und Systemeigenen Paketverwaltungen aufgelöst werden. So passiert es in jüngeren Debian-Distributionen, dass z.B. pip install sphinx mit error: externally-managed-environment begegnet wird, und nur --break-system-packages erlaubt, die Python-Installation zu modifizieren, die von der systemeigenen Paketverwaltung installiert wurde. Was lange erlaubt blieb, Paketinstallationen im Userspace, ist in Debian 12 nun auch ‘verboten’.
venv¶
Eine Lösung für die Dependency Hell ist Containern (Isolation), d.h. eine Anwendung zusammen mit allen Abhängigkeiten in einen Ordner ablegen, und dafür zu sorgen, dass Python nur in diesem Ordner die Pakete verwendet (und nicht etwa andere Versionen davon an anderen Orten auf dem selben Rechner). Eine ‘starke’ Containerisierung wäre ein VM Image (eine maßgeschneiderte Debian-Installation mit genau der richtigen Python-Version und den passenden Paketen im System) oder ein entsprechendes Docker Image. Leichtgewichtiger ist, die Python-Installation eines Systems als gegeben anzunehmen und nur die Pakete, die man benötigt, in einem Container zu isolieren.
Der Befehl venv erstellt einen lokalen Ordner (oft .venv genannt, wobei der Punkt in Unix die Semantik eines ‘versteckten’ Ordners impliziert).
Dieser enthält ein eigenes leeres site-packages-Verzeichnis und Symlinks auf einen Python-Interpreter (der im System installiert ist).
Damit wird in einem venv (=virtual environment) ein Interpreter ausgewählt (wir kontrollieren explizit, welche Python-Version) und es gibt einen dritten Ort neben System und Userspace, in den pip Pakete installieren darf - und das auch in Debian 12.
Ein venv wird benutzt, indem es ‘aktiviert’ wird, was nur bedeutet, dass der System-PATH um den Ordner ergänzt wird, sodass Python-Pakete darin gesucht/installiert werden.
python -m venv .venv # venv anlegen
source .venv/bin/activate # venv aktivieren
python -m pip install requests # ins venv installierenZwei verschiedene Softwareprojekte / Python-Pakete können somit jedes ein eigenes venv erhalten und für sich individuelle Versionen von Abhängigkeiten verwenden.
Etwas ärgerlich ist nun, wenn man 10 Projekte hat, die alle eine Abhängigkeit auf ein großes Paket haben (z.B. wenn man PyTorch einsetzt), dann liegt die große Abhängigkeit 10 Mal auf der Festplatte. Das verschwendet Platz und auch Zeit, wenn die Abhängigkeit heruntergeladen werden muss. Hierzu gibt es offensichtlich Caching-Möglichkeiten, die venv allerdings nicht von Haus aus mitbringt (aber uv).
setup.py, distutils, setuptools, hatchling¶
In den frühen Versionen von Python hat man ein Paket kompiliert / verteilt, indem man eine setup.py Datei geschrieben hat, die mit dem distutils-Paket aus der Standardbibliothek den Installationsprozess durchgeführt hat. In Python 3.12 wurde distutils endgültig abgeschafft und durch setuptools ersetzt. Das macht sich manchmal bemerkbar, wenn man ältere Python-Projekte auf Python >= 3.12 zum Laufen bekommen möchte.
Inzwischen versucht man, die Metadaten eines Pakets und die Installtionsdetails vom Code zu trennen, und verwaltet die Daten daher nicht mehr in setup.py sondern in der pyproject.toml. Diesem Wechsel vom imperativem Ansatz zum deklarativen Ansatz begleitet der Wechsel von setuptools zu hatch/hatchling, welches auch von der Python Package Authority (PyPA) verwaltet wird.
pyproject.toml¶
In PEP 621 ist beschrieben, wie ein Python-Projekt seine Metadaten so formuliert, dass Packaging-Werkzeuge diese verarbeiten können. Dabei wurde das TOML-Format gewählt, weil es menschenlesbar ist, im Gegensatz zu JSON auch Kommentare erlaubt und nicht so strikte Formatierungsregeln wie YAML einfordert und leichter zu parsen ist.
Ein Beispiel einer (kleinen) Projektkonfiguration:
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "my_minimal_package"
version = "0.1.0"
description = "A minimal example Python package"
authors = [
{ name = "Your Name", email = "you@example.com" }
]
dependencies = [
"requests>=2.30.0"
]Eggs und Wheels¶
Ein .egg war der erste Anlauf, in Python Pakete (als Zip-Archive) zu verteilen; Eggs werden heute nicht mehr verwendet.
Wheels .whl sind das aktuelle Distributionsformat für Python. Ein Wheel ist ein Zip-Archiv, welches vorkompilierten Python-Code (Bytecode, .pyc-Dateien) zusammen mit vorkompiliertem C-Code oder Rust-Code enthält. Somit kann pip install direkt ein lauffähiges Paket bereitstellen, auch wenn im System keine passenden C- oder Rust-Compiler vorliegen.
Poetry und Pipx¶
Anstatt manuell ein venv anzulegen und mit pip die Abhängigkeiten in requirements.txt händisch zu verwalten, kann man die Metadaten (Abhängigkeiten) in der pyproject.toml beschreiben und mit einem High-Level Projektverwaltungswerkzeug wie Poetry bearbeiten. Poetry automatisiert nicht nur die Erstellung der virtuellen Umgebung und die strikte Auflösung von Abhängigkeitskonflikten (via Lockfile), sondern kümmert sich auch ganzheitlich um das Bauen und Veröffentlichen (Publishing) von Paketen auf PyPI. Damit kapselt man den venv+pip-Workflow für reproduzierbare Builds. Eine ältere Alternative war pipenv.
Ein weiteres Werkzeug ist pipx. Während pip (oder Poetry) primär Bibliotheken in der Entwicklungsumgebung installiert, ist pipx speziell dafür gedacht, ausführbare Endnutzer-Kommandozeilenanwendungen (CLI-Tools wie black, mypy oder ruff) global im System bereitzustellen. Der Clou: pipx verpackt jedes Tool automatisch in ein eigenes, unsichtbares venv. So können diese global aufgerufen werden, ohne jemals die systemweiten Pakete oder andere Tools durch inkompatible Abhängigkeiten zu stören.
Diese Werkzeuge (und ihre spezifischen Workflows) werden heutzutage im Wesentlichen durch das allumfassende uv abgelöst.
uv¶
Uv ist ein schneller Python Paket- und Projektmanager, der in Rust geschrieben ist. Es ist das Aushängeschild für Oxidization in der Python-Welt, das ist der Trend, gewisse Komponenten in Rust zu implementieren, für mehr Sicherheit und Geschwindigkeit. Uv ersetzt u.a. pip, pipx, poetry, virtualenv und ist dabei um Faktor 10 bis 100 mal schneller als pip.
Unix:
curl -LsSf https://astral.sh/uv/install.sh | shWindows:
powershell -c "irm https://astral.sh/uv/install.ps1 | more"Der Workflow ist, die ‘alten’ Befehle mit ‘uv’-Varianten zu ersetzen:
uv pip show requests
uv pip install requests
uv pip list --outdated
uvx mypy # like pipxBeispiel¶
Um ein Programm mit seinen Abhängigkeiten zu installieren / zu starten ist ein typischer Workflow:
sudo apt install freeglut3-dev # install external dependencies!
git clone https://github.com/konradvoelkel/wesen.git
cd wesen
uv venv
uv sync
uv run wesenDie wesentliche Arbeit macht uv sync, denn dabei wird die pyproject.toml eingelesen, ein Lockfile uv.lock erstellt mit präzisen Versionsangaben aller Abhängigkeiten sowie Quellen für die Pakete. Der Befehl uv run wesen ruft ein zuvor in der pyproject.toml konfiguriertes Skript auf.
Ein Beispiel einer pyproject.toml mit uv-spezifischen Anweisungen (siehe auch die Folgekapitel):
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "wesen"
version = "0.7.0"
description = "A game where players program Wesen behavior."
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"numpy",
"PyOpenGL>=3.1.0",
"Pillow>=12.2.0",
]
[project.scripts]
# Creates a terminal command 'wesen' that runs Loader()
wesen = "Wesen.loader:Loader"
[dependency-groups]
dev = [
"mypy>=2.1.0",
"ruff>=0.15.13",
]
Inline-Metadaten:
Besonders elegant für einzelne, in sich geschlossene Skripte (Self-contained scripts) ist die durch PEP 723 standardisierte Möglichkeit, Metadaten wie Paketabhängigkeiten direkt als speziellen Kommentarblock oben in der .py-Datei abzulegen.
Ruft man ein solches Skript beispielsweise mit uv run mein_skript.py auf, liest uv diese Metadaten, baut on-the-fly eine temporäre, isolierte Umgebung mit exakt diesen Paketen, führt das Skript aus und räumt danach wieder auf – völlig ohne dass dafür ein Ordner, eine pyproject.toml oder ein manuelles venv angelegt werden muss.
Dazu sieht der Anfang von mein_skript.py so aus:
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "requests>=2.32.0",
# ]
# ///
print("Hier kommt normaler Python-Code")