1
 
 
Profil
In deinem persönlichen Profilbereich kannst du den Status deiner Bewerbung einsehen, unvollständige Bewerbungen zwischenspeichern und aktuelle News und Events einsehen
26. März 2025

Wie UV das schwierige Problem von Pythons Paketverwaltung löst

Worum geht es in dem Artikel?

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.

Warum ist Pythons Paketverwaltung so schwierig?

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: 

  • Es gibt ein flaches Layout und ein src-Layout
  • Ihr könnt den Code als Python-Paket oder als Sammlung von Skripten organisieren. 
  • Paketdefinitionen können in einer setup.py oder einer neuen pyproject.toml abgelegt werden. 
  • Außerdem setzt die Python Packaging Authority (PyPa) keinen einzelnen Standard für die Paketverwaltung durch. Stattdessen ist es ein demokratischer Standard mit mehreren Build-Backend- und Frontend-Implementierungen – hatch, setuptools, pdm, um nur einige zu nennen. 
  • PyPa prüft auch nicht, ob hochgeladene Pakete in ihrem öffentlichen Repository tatsächlich das installieren, was ihre Metadaten als transitive Abhängigkeiten angeben.

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.

Was sollte man von modernen Paketierungslösungen erwarten?

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:

  • Installation mehrerer Pythons und Verwendung einer eigenen Python-Version pro Projekt.
  • Isolierung von 3rd-Party-Paketen pro Projekt.
  • Organisieren des Codes unter einem obersten importierbaren Modul-Namensraum
  • Verwenden einer deterministische Locking-Lösung.
  • Installieren gleicher Versionen von transitiven Abhängigkeiten auf verschiedenen Plattformen – z.B. Apple Silicon arm64 für die Entwicklung und Linux amd64 für Produktion.
  • Ermöglichung eines toolgestützten Upgrades von Abhängigkeiten durch Verwendung semantischer Versionsrichtlinien.
  • Automatisierte Aktualisierungen von Abhängigkeiten und CVE-Scans aller transitiven Abhängigkeiten.

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.

Warum UV und nicht X?

Hier eine kurze Liste der wichtigsten Argumente für UV:

  • UV verfügt über einen effizienten Algorithmus zur Abhängigkeitsauflösung, geschrieben in Rust, um das NP-schwere Problem zu bewältigen.
  • UV vereint die Installation von Python, das Isolieren von Abhängigkeiten (virtuelle Umgebung), das Installieren von Paketen (pip) und die projektspezifische Auflösung (venv activate).
  • UV unterstützt plattformübergreifende deterministische Lock-Dateien, um gleiche Versionen transitiver Abhängigkeiten auf verschiedenen Betriebssystemen und CPU-Architekturen zu installieren.

Und hier die Nachteile anderer Tools:

Pip + venv + pip-tools

  • Erfordert, dass jedes Teammitglied eine Reihe von Tools installiert und lernt.
  • Manuelles Verwalten virtueller Umgebungen und Aktivieren beim Wechsel zwischen Projekten.
  • Der Entwicklungs-Workflow wird nicht durch Tools erzwungen und muss als Team-Best-Practice etabliert werden.
  • Lock-Dateien sind nicht plattformübergreifend und können nur für die Plattform erstellt werden, auf der pip-tools ausgeführt wurde. Eine vollständige requirements.txt mit allen transitiven Abhängigkeiten, die auf Apple Silicon erstellt wurde, kann z.B. Pakete enthalten, die auf Linux amd64 nicht installierbar sind und zu fehlerhaften Builds führen.

Poetry

  • Kann aufgrund der großen Auflösungsmöglichkeiten langsam werden, bis es überhaupt nicht mehr benutzbar ist.
  • Es können falsche Abhängigkeiten installiert werden, wenn Paketautoren die Metadaten auf PyPa nicht konsistent mit den tatsächlichen Abhängigkeiten, die ein Paket installiert, deklarieren.

Conda

  • Verwendet ein eigenes Paketformat und Repositories, die weniger Pakete enthalten als PyPa (pip).
  • Um das oben genannte Problem zu beheben, wird eine Pip-Interoperabilitätslösung ausgeliefert, die in der Praxis oft dazu führt, dass die transitiven Abhängigkeiten von Conda versehentlich mit Pip überschrieben werden, was zu fehlerhaften Builds führt.
  • Hat weder plattformübergreifendes, noch deterministisches Locking, das mit einer Semver-Policy zusammenarbeitet und kleinere Versions-Upgrades erlaubt (ein Werkzeug, das dies in der Conda-Welt adressiert, ist Mamba).

Wie organisiert man ein Python-Projekt mit UV?

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

Wie teilt und deployed man Code?

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

Wie werden die Pakete aktualisiert?

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"

Wie lassen sich Aktualisierungen von Abhängigkeiten automatisieren?

Es gilt als bewährte Praxis, Abhängigkeiten regelmäßig zu aktualisieren, um:

  • Schwachstellen zu vermeiden, 
  • Inkompatibilitäten zwischen Abhängigkeiten einzuschränken,
  • komplexe Upgrades zu vermeiden, wenn von einer zu alten Version aktualisiert wird

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.

Zusammenfassung

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?

15 Personen gefällt das

0Noch keine Kommentare

Dein Kommentar
Antwort auf:  Direkt auf das Thema antworten

Geschrieben von

Andreas Winschu
Andreas Winschu
Data Engineer

Ähnliche Beiträge

Gespeichert!

We want to improve out content with your feedback.

How interesting is this blogpost?

We have received your feedback.