Python wurde de facto zum Standard für maschinelles Lernen, vor allem wegen dessen leicht zugänglichen Programmierschnittstellen. Paradoxerweise, wenn es darum geht den finalen Code auszuliefern, stellt sich die Paketverwaltung in Python als alles andere als einfach heraus. Es wurden zwar schon mehrere Ansätze zur Vereinfachung dieser Aufgabe ausprobiert, aber unsere Erfahrungen mit dem neuen UV-Paketmanager sind mehr als vielversprechend. Dieser Artikel zeigt, warum ihr UV für die Organisation der Abhängigkeiten eures Python-Projekts wählen solltet und welche Vorteile UV mit sich bringt.
Das berühmte xkcd-Comic „Standards“ ist eine verblüffend genaue Beschreibung des Zustands der Python-Paketverwaltung. Während das Credo der Programmiersprache Python selbst lautet: „There should be only one way to do it“, ist nichts weiter davon entfernt als die Myriaden von Optionen, die es gibt, um das Projektlayout von Python-Software zu organisieren.
Hier mal einige Beispiele zur Veranschaulichung:
Aufgrund der oben genannten, historisch eingeführten Optionen ist die Aufgabe, die Paketverwaltung in Python zu lösen, schwierig – sogar NP-schwer. Paketmanager sind gezwungen, entweder Heuristiken darüber zu entwickeln, wo sie nach Abhängigkeiten suchen sollen, oder tatsächlich große Binärdateien herunterzuladen, bevor sie den vollständigen Abhängigkeitsbaum auflösen.
Vor kurzem haben wir mehrere Python-Projekte für maschinelles Lernen standardisiert. Unser Ziel war es, etablierte Standards für die Paketverwaltung aus anderen Programmierumgebungen wie Node, Golang, Ruby oder Rust zu erreichen:
Derzeit verwenden wir immer noch eine Mischung aus pip, venv, pip-compile, direnv und dem Systempaketmanager für die Installation von Python. Dies hat nicht nur den Nachteil, dass jede*r im Team lernen muss, mit dieser Mischung von Werkzeugen umzugehen, sondern auch, dass es keine einzige plattformübergreifende Lockdatei gibt.
Der neue Paketmanager "UV" zeigt Potenzial, die Tool-Vielfalt zu reduzieren, Workflows zu vereinfachen und die Abhängigkeitsverwaltung für Entwickler*innen zu straffen. Nachfolgend wird erklärt, wie UV diese Vorteile erreicht.
Hier eine kurze Liste der wichtigsten Argumente für UV:
Und hier die Nachteile anderer Tools:
Pip + venv + pip-tools
Poetry
Conda
UV verwaltet den gesamten Python-Entwicklungs-Workflow und abstrahiert die Verwaltung virtueller Umgebungen, indem Python-Befehle über einen UV-Wrapper-Befehl ausgeführt werden.
So initialisiert ihr ein Projekt namens "uv-light":
uv python install 3.12
uv init uv-light —python 3.12
Ihr fügt folgende Abhängigkeiten hinzu:
uv add flask pandas pyarrow
Die obigen Befehle erstellen ein minimales Paketverzeichnis-Layout gemäß dem PEP 621-Standard für Projektkonfiguration in einer pyproject.toml-Datei. PEP 508-Abhängigkeitsspezifikationen (bekannt als requirements.txt) werden in den Abschnitt dependencies eingetragen: "flask>=3.0.3", "pandas>=2.2.1", "pyarrow>=17.0.0".
Es ist empfehlenswert, die >=-Spezifikationen auf eine striktere == x.*-SemVer-Richtlinie umzustellen, die die Hauptversionen sperrt. Dies ermöglicht später ein Upgrade aller minor Versionen im gesamten transitiven Abhängigkeitsbaum. Ein solches Update zielt darauf ab, jedes Paket auf die neueste Version zu aktualisieren, die die Build-Kompatibilität gemäß SemVer nicht bricht.
Resultierende pyproject.toml-Konfiguration:
[project]
name = "uv-light"
# ...
requires-python = ">=3.12"
dependencies = [
"flask==3.*",
"pandas==2.*", # Jede Pandas 2, aber nicht 3
"pyarrow==17.*",
]
Der Code kann nun in folgendem Verzeichnislayout organisiert werden:
./uv-light
├── uv_light
│ ├──__init__.py
│ ├── lens.py
│ ├── beam.py
├── main.py
├── pyproject.toml
└── uv.lock
Dieses Layout ermöglicht ein import from uv_light:
# main.py
from uv_light.lens import Lens
from uv_light.beam import Beam
Beam().project_on(Lens())
Um die main.py-Datei in ihrer virtuellen Umgebung auszuführen, verwendet ihr den Befehl uv run:
uv run main.py
Ein anderes Teammitglied kann nun das Projekt auschecken und einfach den oben genannten Befehl uv run ausführen. UV sorgt dafür, dass genau dieselben Python-Versionen und Pakete installiert werden, die zur Erstellung dieses Programms verwendet wurden. Zu diesem Zweck erzeugt UV eine plattformübergreifend Lock-Datei uv.lock, die in das Quellcode-Repository eingecheckt wird.
Dieselbe Strategie kann für die Produktionsbereitstellung verwendet werden, wenn UV in der Produktionsumgebung, z. B. in einem Docker-Container, installiert ist. Einige Cloud-Dienste erfordern möglicherweise noch weiterhin eine requirements.txt Abhängigkeitsspezifikation. Für diesen Anwendungsfall bietet UV eine pip-kompatible Schnittstelle mit einem uv export Befehl, der verschiedene Zielplattformen unterstützt.
Um eine requirements.txt zu erstellen, die alle transitiven Abhängigkeiten für jede Plattform auflistet, führt ihr folgendes aus:
uv export --no-hashes -o requirements.txt
UV kann alle Pakete innerhalb einer definierten Richtlinie in pyproject.toml aktualisieren. Dies ermöglicht eine rückwärtskompatible Upgrade-Strategie. Auf diese Weise werden die Versionsbereiche der direkt benutzten Pakete respektiert. Alle transitiven Abhängigkeiten werden auf die neueste kompatible Version zu den direkt benutzten Paketen und untereinander aktualisiert.
Um ein solches Upgrade durchzuführen, führt ihr folgendes aus:
uv lock –upgrade
Im Beispiel dieses Artikels würden Pandas bei Version 2, Flask bei 3 und PyArrow bei 17 bleiben. Ein SemVer-kompatibles Update könnte folgende Änderungen umfassen:
Updated flask v3.0.3 -> v3.1.0
Updated pandas v.2.2.1 -> 2.2.3
Updated numpy v1.26.4 -> v2.1.3 # transitive
Wenn ihr bereit seid, ein Major Update durchzuführen, patcht ihr den betroffenen Code einer nicht abwärtskompatiblen API-Änderung und erhöht manuell die Hauptversion im Abschnitt pyproject.toml dependencies.
Danach führt ihr uv lock erneut aus. Dies wird zu der folgenden Änderung führen:
--- a/pyproject.toml
+++ b/pyproject.toml
dependencies = [
"flask==3.*",
"pandas==2.*",
- "pyarrow==17.*",
+ "pyarrow==18.*",
]
--- a/uv.lock.toml
+++ b/uv.lock.toml
[[package]]
name = "pyarrow"
-version = "17.0.0"
+version = "18.0.0"
Es gilt als bewährte Praxis, Abhängigkeiten regelmäßig zu aktualisieren, um:
Eine weit verbreitete Lösung zur Aktualisierung von Abhängigkeiten ist Dependabot. Leider unterstützt Dependabot zum Zeitpunkt der Erstellung dieses Artikels uv noch nicht. GitHub hat jedoch zugesagt, UV-Support in Dependabot zu integrieren. Der Fortschritt kann hier verfolgt werden.
Für frühe Anwender*innen von UV, die auf Dependabot angewiesen sind, kann ein pip-compile Workflow auf die gleiche Weise verwendet werden, wie oben beschrieben, um die ältere requirements.txt Spezifikation zu erzeugen. Der pip-compile-Workflow wird von Dependabot erstklassig unterstützt und erwartet die gleiche Projekt-Layout-Struktur wie die von UV generierte: eine pyproject.toml mit einem Abschnitt für Abhängigkeiten und eine requirements.txt-Datei mit gelockten Abhängigkeiten.
Dependabot erkennt einen pip-compile-Setup anhand eines Kommentars in der generierten requirements.txt. Ändert den von UV generierten Kommentar wie folgt:
#
# This file is autogenerated by pip-compile with Python 3.12
# by the following command:
#
# pip-compile pyproject.toml
#
Fügt eine entsprechende .github/dependabot.yml-Konfiguration hinzu:
version: 2
updates:
- package-ecosystem: "pip"
schedule:
interval: "daily"
groups:
patches:
update-types:
- "minor"
- "patch"
open-pull-requests-limit: 100
Dependabot erstellt nun Pull-Requests mit Updates in requirements.txt und pyproject.toml. Die obige Konfiguration fasst nicht-brechende Kompatibilitätsaktualisierungen in einem einzigen Pull Request zusammen. Zusätzlich wird Github veranlasst, alle in requirements.txt aufgeführten Abhängigkeiten nach CVEs zu scannen. Dieser Schritt kann auch auf CI automatisiert werden. Beispiele findet ihr unter uv-sync.sh und workflows/push.vml.
Python hat historisch viele Packaging-Optionen. Drittanbieter*innen versuchten, das Packaging-Problem zu lösen, was zu einem fragmentierten Ökosystem führte. UV hingegen ist ein vielversprechender neuer Ansatz, der plattformübergreifende deterministische Lock-Dateien und automatische Umgebungsisolierung bietet, um homogene Entwicklungsumgebungen zu ermöglichen.
Ihr möchtet euch in ein vollständiges Beispiel-Repository mit den in diesem Artikel beschriebenen Konzepten anschauen? Das findet ihr hier.
Möchtest du Teil des Teams werden?

We have received your feedback.