cat posts/uv-python-paketmanager.md
uv: pip und venv in schnell
uv bündelt Paketinstallation, virtuelle Umgebungen, Projektverwaltung, Python-Versionen und isolierte Tools – schnell, reproduzierbar und mit modernen Workflows für pyproject.toml und uv.lock.
In Teil 10 der Python-Serie
haben wir venv und pip kennengelernt:
python -m venv .venv
source .venv/bin/activate
python -m pip install rich
Das ist der klassische Python-Werkzeugkasten. Er funktioniert weiterhin und ist wichtig zu verstehen.
Bei einem größeren Projekt kommen allerdings schnell weitere Werkzeuge dazu:
venverzeugt die virtuelle Umgebung,pipinstalliert Pakete,- ein zusätzliches Tool erzeugt reproduzierbare Lockfiles,
pipxkümmert sich um isolierte Kommandozeilen-Tools,pyenvoder ähnliche Tools verwalten Python-Versionen.
uv bündelt einen großen Teil davon in einem einzigen Programm.
Es verwaltet Projekte und Abhängigkeiten, erzeugt virtuelle Umgebungen und Lockfiles, kann Python selbst installieren und startet Tools oder einzelne Skripte in isolierten Environments.
Und es ist schnell. Sehr schnell.
Der interessantere Unterschied ist im Alltag aber nicht die Geschwindigkeit, sondern der Workflow:
uv add rich
uv run python main.py
Mehr braucht es für viele Projekte tatsächlich nicht.
Was uv ist
uv ist ein eigenständiger Python-Paket- und Projektmanager von Astral, den Machern von Ruff.
Das Programm selbst ist in Rust geschrieben und benötigt keine vorhandene Python-Installation, um zu starten. Falls nötig, kann uv sogar den passenden Python-Interpreter herunterladen.
Zu seinen Aufgaben gehören unter anderem:
virtuelle Umgebungen
Abhängigkeiten
Lockfiles
Python-Versionen
Kommandozeilen-Tools
PEP-723-Skripte
Builds und Publishing
Workspaces
Dabei gibt es zwei unterschiedliche Ebenen, die man auseinanderhalten sollte:
Projektverwaltung
uv init
uv add
uv remove
uv lock
uv sync
uv run
niedrigere pip-kompatible Schnittstelle
uv venv
uv pip install
uv pip sync
uv pip compile
Für neue Projekte ist normalerweise die erste Gruppe interessant.
uv pip ist vor allem praktisch für bestehende Workflows mit
requirements.txt, virtuellen Umgebungen oder Tools, die bisher direkt mit
pip gearbeitet haben.
Warum uv so schnell ist
uv ist in Rust implementiert und darauf ausgelegt, möglichst viel Arbeit parallel zu erledigen und bereits heruntergeladene Dateien aggressiv wiederzuverwenden.
Unter anderem cached uv:
- heruntergeladene Pakete,
- Wheels,
- gebaute Artefakte,
- Metadaten.
Der globale Cache kann Dateien außerdem zwischen verschiedenen Environments wiederverwenden.
Bei:
uv add rich
muss also nicht jedes Projekt sämtliche Dateien erneut herunterladen und kopieren.
Bei einem kleinen Lernprojekt spart das vielleicht nur einen Moment. In CI, Containern oder größeren Dependency Graphs wird der Unterschied deutlich interessanter.
Trotzdem würde ich uv nicht allein wegen eines Benchmarks verwenden. Der größere Vorteil ist, dass dieselben Kommandos lokal, in CI und auf einem neuen Rechner denselben Projektzustand herstellen können.
uv installieren
Unter Linux und macOS bietet Astral einen Standalone Installer an:
curl -LsSf https://astral.sh/uv/install.sh | sh
Wenn Du ein Installationsskript aus dem Internet nicht ungeprüft an die Shell reichen möchtest, kannst Du es vorher ansehen:
curl -LsSf https://astral.sh/uv/install.sh | less
Unter Windows PowerShell:
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
Daneben gibt es Pakete für verschiedene Package Manager, beispielsweise:
brew install uv
oder unter Windows:
winget install --id=astral-sh.uv -e
Auch eine Installation über PyPI ist möglich:
pipx install uv
Der Standalone Installer hat allerdings den angenehmen Effekt, dass uv komplett unabhängig von einer bereits vorhandenen Python-Installation ist.
Danach:
uv --version
uv aktualisieren
Wurde uv über den Standalone Installer installiert:
uv self update
Bei einer Installation über einen Package Manager aktualisierst Du uv dagegen mit dessen Mechanismus.
Also beispielsweise:
brew upgrade uv
oder:
pipx upgrade uv
uv self update ist nicht für jede Installationsmethode vorgesehen.
Zwei grundsätzliche Arbeitsweisen
uv kann ziemlich unterschiedlich benutzt werden.
Projektmodus
Für neue Projekte:
uv init
uv add rich
uv run ...
uv sync
Hier verwaltet uv:
pyproject.toml
uv.lock
.venv
weitgehend gemeinsam.
Das ist der Workflow, auf den ich mich in diesem Artikel hauptsächlich konzentriere.
pip-kompatibler Modus
Für bestehende oder bewusst manuell verwaltete Environments:
uv venv
uv pip install rich
Das fühlt sich ähnlich wie:
python -m venv .venv
pip install rich
an.
Wichtig: uv pip ruft intern nicht einfach pip auf. uv implementiert diese
Schnittstelle selbst und ist auch nicht in jedem Detail identisch mit pip.
uv init hat mehrere Projektarten
An dieser Stelle hat sich uv in den letzten Versionen deutlich verändert.
Ein einfaches:
uv init mein-projekt
erzeugt heute standardmäßig eine Application, die als Python-Package installiert werden kann.
Eine typische Struktur sieht ungefähr so aus:
mein-projekt/
├── .git/
├── .gitignore
├── .python-version
├── README.md
├── pyproject.toml
└── src/
└── mein_projekt/
└── __init__.py
Die pyproject.toml enthält dabei zusätzlich ein Build-System und einen
Command Entry Point.
Du kannst die Anwendung anschließend beispielsweise so starten:
cd mein-projekt
uv run mein-projekt
Für eine richtige Application oder ein später installierbares CLI ist das ein guter Default.
Ein einfaches Projekt mit main.py
Für unseren Lerncode möchte ich es zunächst etwas schlichter halten.
Dafür gibt es:
uv init --no-package mein-projekt
Danach:
cd mein-projekt
Die Struktur sieht ungefähr so aus:
mein-projekt/
├── .git/
├── .gitignore
├── .python-version
├── README.md
├── main.py
└── pyproject.toml
In main.py liegt bereits ein kleines Beispielprogramm.
Die pyproject.toml enthält ungefähr:
[project]
name = "mein-projekt"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []
Die genaue Python-Version hängt von dem Interpreter ab, den uv beim Initialisieren auswählt.
Für die folgenden Beispiele verwenden wir diese einfache
--no-package-Variante.
Eine Abhängigkeit hinzufügen
Installieren wir rich:
uv add rich
Damit passiert mehr als bei einem einfachen:
pip install rich
uv:
- ergänzt die Abhängigkeit in
pyproject.toml, - löst ihre Dependencies auf,
- erzeugt oder aktualisiert
uv.lock, - erzeugt bei Bedarf
.venv, - synchronisiert die Projektumgebung.
In der pyproject.toml steht danach beispielsweise:
[project]
dependencies = [
"rich>=...",
]
Die tatsächlich eingetragene Versionsgrenze hängt vom aktuellen Paketstand ab.
Der entscheidende Punkt ist:
Die Dependency wird Teil des Projekts und nicht nur zufällig in irgendeinem Environment installiert.
Code mit uv run ausführen
In main.py:
from rich import print
print("[bold green]Willkommen im Dungeon![/bold green]")
Start:
uv run python main.py
Bei einer Python-Datei geht auch:
uv run main.py
uv sorgt vor dem Start dafür, dass das Projekt-Environment existiert und zum Projekt passt.
Eine manuelle Aktivierung wie:
source .venv/bin/activate
ist dafür nicht notwendig.
Was uv run vor dem Start macht
Ein:
uv run python main.py
führt nicht einfach blind .venv/bin/python aus.
uv prüft vorher automatisch:
Passt uv.lock noch zur pyproject.toml?
↓
Passt .venv noch zum Lockfile?
↓
Falls nötig locken und synchronisieren
↓
Kommando ausführen
Deshalb ist:
uv run pytest
oder:
uv run ruff check .
so praktisch.
Die Umgebung wird Teil des Kommandos und nicht zu einem unsichtbaren Zustand Deiner Shell.
Warum nicht einfach python main.py?
Natürlich kannst Du weiterhin:
python main.py
verwenden.
Dann entscheidet allerdings Deine Shell, welches python gemeint ist.
Ist die virtuelle Umgebung aktiviert:
.venv
passt das wahrscheinlich.
Ist sie nicht aktiviert, kann stattdessen verwendet werden:
System-Python
anderes venv
Conda-Environment
anderer Interpreter aus PATH
Mit:
uv run python main.py
ist ausdrücklich das Environment des uv-Projekts gemeint.
Das ist vor allem in Dokumentation und CI angenehm, weil niemand erst daran denken muss:
source .venv/bin/activate
auszuführen.
.venv existiert trotzdem
uv schafft virtuelle Umgebungen nicht ab.
Das Projekt besitzt weiterhin normalerweise:
.venv/
Dort liegen Interpreter und installierte Pakete.
Du kannst das Environment sogar wie gewohnt aktivieren:
source .venv/bin/activate
Unter PowerShell:
.venv\Scripts\Activate.ps1
Du musst es für uv aber meistens nicht.
Das ist ein wichtiger Unterschied:
uv ersetzt nicht das Konzept virtueller Environments. Es nimmt Dir nur viel von deren manueller Verwaltung ab.
pyproject.toml und uv.lock
Die beiden wichtigsten Dateien erfüllen unterschiedliche Aufgaben.
In pyproject.toml stehen die Anforderungen des Projekts:
dependencies = [
"rich>=14",
]
Das bedeutet nicht:
Installiere exakt Rich 14.2.0 und exakt diese drei transitiven Dependencies.
Es beschreibt einen erlaubten Bereich.
uv.lock hält dagegen die von uv konkret aufgelöste Dependency-Struktur fest.
Die Lockdatei ist:
- maschinenlesbar,
- trotzdem als TOML lesbar,
- von uv verwaltet,
- für Git gedacht.
Du solltest sie normalerweise nicht von Hand bearbeiten.
uv.lock ist ein universelles Lockfile
Eine wichtige Besonderheit ist, dass uv.lock nicht einfach nur den Zustand
Deines aktuellen Rechners einfriert.
uv löst Dependencies universell auf.
Das Lockfile kann beispielsweise Informationen für unterschiedliche:
Python-Versionen
Betriebssysteme
Architekturen
Environment Marker
enthalten.
Angenommen, Dein Projekt unterstützt:
requires-python = ">=3.9"
Ein Paket benötigt aber für verschiedene Python-Versionen unterschiedliche Releases.
Dann kann dasselbe uv.lock mehrere Varianten dieses Pakets enthalten.
Vereinfacht:
Python 3.9 → paket 1.x
Python >= 3.10 → paket 2.x
Die Aussage:
Im Lockfile steht für jedes Paket genau eine Versionsnummer.
wäre deshalb zu einfach.
Besser:
uv.lockenthält die exakt aufgelöste Dependency-Struktur für die von Deinem Projekt unterstützten Umgebungen.
Gehört uv.lock ins Git?
Für Anwendungen normalerweise:
ja
Also:
pyproject.toml
uv.lock
committen.
Nicht committen:
.venv/
Die virtuelle Umgebung lässt sich aus den Projektdateien wiederherstellen.
Eine typische .gitignore enthält beispielsweise:
.venv/
__pycache__/
*.pyc
uv init legt für neue Projekte bereits eine passende .gitignore an.
Eine Umgebung mit uv sync herstellen
Nach einem Git-Clone:
git clone <repository>
cd <repository>
uv sync
uv erzeugt bei Bedarf .venv und installiert den Zustand aus dem Projekt.
Danach:
uv run python main.py
Oft könntest Du uv sync sogar überspringen:
uv run python main.py
synchronisiert bei Bedarf selbst.
uv sync ist trotzdem praktisch, wenn Du das Environment bewusst vorbereiten
möchtest, beispielsweise für einen Editor.
uv sync ist standardmäßig exakt
Hier gibt es eine interessante Feinheit.
uv sync
führt standardmäßig einen exact sync durch.
Pakete, die nicht zum gelockten Projektzustand gehören, können dabei aus der Projektumgebung entfernt werden.
Das ist Absicht:
Environment soll dem Projekt entsprechen.
Wenn Du zusätzliche Pakete behalten möchtest:
uv sync --inexact
Beim normalen Projektmanagement solltest Du solche Pakete aber besser gar nicht
manuell in .venv hineininstallieren.
uv run synchronisiert etwas konservativer
uv run verwendet beim automatischen Sync standardmäßig ein inexact
Verhalten.
Es stellt sicher, dass die benötigten Pakete vorhanden sind, entfernt aber nicht bei jedem Start irgendwelche zusätzlich vorhandenen Pakete.
Wenn Du auch dort einen exakten Sync möchtest:
uv run --exact python main.py
Im normalen Alltag musst Du diesen Unterschied selten bewusst steuern.
Er erklärt aber, warum:
uv sync
stärker aufräumen kann als:
uv run ...
Lockfile nicht unbemerkt ändern
Standardmäßig darf uv das Lockfile aktualisieren, wenn sich die
pyproject.toml geändert hat.
In CI möchtest Du häufig stattdessen prüfen:
Ist das eingecheckte Lockfile tatsächlich aktuell?
Dafür gibt es:
uv sync --locked
Ist uv.lock nicht mit dem Projekt vereinbar, schlägt der Befehl fehl.
Dasselbe funktioniert mit uv run:
uv run --locked pytest
--locked und --frozen
Die beiden Optionen klingen ähnlich, bedeuten aber etwas Unterschiedliches.
--locked
bedeutet:
Prüfe, ob das Lockfile aktuell ist, aber ändere es nicht.
--frozen
bedeutet:
Benutze das vorhandene Lockfile, ohne zu prüfen, ob es zur aktuellen Projektdefinition passt.
Für eine normale CI-Prüfung ist --locked meistens die interessantere
Variante.
Abhängigkeiten aktualisieren
Ein vorhandenes Lockfile verhindert bewusst, dass bei jedem Sync automatisch alle Packages auf neue Versionen springen.
Möchtest Du alle gelockten Dependencies aktualisieren:
uv lock --upgrade
Nur ein Paket:
uv lock --upgrade-package rich
Oder auf eine bestimmte Version:
uv lock --upgrade-package rich==14.2.0
Dabei gelten weiterhin die Versionsgrenzen aus Deinem Projekt.
Das ist angenehm, weil:
uv sync
nicht gleichzeitig bedeutet:
Bitte suche bei jedem Lauf spontan nach allen neuesten Versionen.
Abhängigkeiten entfernen
Ein Paket entfernst Du mit:
uv remove rich
uv passt dabei Projektdefinition, Lockfile und Environment an.
Das ist besser als:
uv pip uninstall rich
wenn rich eine echte Projektabhängigkeit ist.
uv remove sagt:
Dieses Projekt benötigt Rich nicht mehr.
uv pip uninstall sagt nur:
Entferne dieses Paket aus einem Environment.
Das sind zwei unterschiedliche Ebenen.
Development Dependencies
Ruff, pytest oder mypy werden normalerweise nicht von Deiner Anwendung importiert.
Sie gehören zur Entwicklung.
Mit uv:
uv add --dev ruff pytest
uv legt diese Dependencies in der standardisierten Dependency Group dev ab:
[dependency-groups]
dev = [
"pytest>=...",
"ruff>=...",
]
Die normalen Runtime-Abhängigkeiten bleiben unter:
[project]
dependencies = [
...
]
Die dev-Gruppe wird von uv standardmäßig mit synchronisiert.
Danach:
uv run ruff check .
uv run pytest
Mehrere Dependency Groups
Ein größeres Projekt kann die Entwicklungswerkzeuge auch feiner aufteilen:
[dependency-groups]
lint = [
"ruff",
]
test = [
"pytest",
]
dev = [
{ include-group = "lint" },
{ include-group = "test" },
]
Dadurch lässt sich beispielsweise nur eine bestimmte Gruppe installieren:
uv sync --group lint
oder ausschließlich:
uv sync --only-group lint
Für ein kleines Projekt reicht meistens:
uv add --dev ...
vollkommen aus.
Ein Dependency Tree
Wenn Du wissen möchtest, warum ein bestimmtes Paket installiert wird:
uv tree
Das zeigt den aufgelösten Dependency Graph.
Beispielsweise sinngemäß:
mein-projekt
└── rich
├── markdown-it-py
│ └── mdurl
└── pygments
Das ist deutlich angenehmer, als in .venv herumzusuchen.
Einmalig ein zusätzliches Paket verwenden
Nicht jede Dependency muss dauerhaft ins Projekt.
Für einen einzelnen Lauf gibt es:
uv run --with rich python skript.py
Oder mit einer bestimmten Version:
uv run --with "rich<14" python skript.py
Das zusätzliche Paket wird für diesen Aufruf bereitgestellt, ohne dass Du es mit:
uv add rich
in die normalen Projektabhängigkeiten einträgst.
Das ist beispielsweise praktisch für kurze Experimente.
Der niedrigere Modus: uv venv und uv pip
Nicht jedes Projekt soll sofort auf pyproject.toml und uv.lock umgestellt
werden.
Dann kannst Du uv näher am klassischen Workflow benutzen:
uv venv
uv pip install rich
uv venv erzeugt standardmäßig:
.venv/
Danach findet uv pip dieses Environment sogar dann, wenn es nicht aktiviert
ist.
Du kannst also direkt:
uv pip install rich
aufrufen.
Mit einer requirements.txt:
uv pip install -r requirements.txt
Aktivieren kannst Du das Environment bei Bedarf weiterhin:
source .venv/bin/activate
uv pip installiert nicht einfach ins System-Python
Klassisches pip kann ohne aktives venv schnell beim globalen Interpreter
landen.
uv pip install verhält sich absichtlich vorsichtiger.
uv sucht zunächst unter anderem:
aktives Virtual Environment
aktives Conda Environment
.venv im aktuellen oder übergeordneten Verzeichnis
Findet es kein geeignetes Virtual Environment, installiert es nicht einfach blind in das System-Python.
Ein System-Environment muss ausdrücklich gewählt werden, beispielsweise mit:
uv pip install --system rich
Das sollte genauso bewusst geschehen wie bei anderen Python-Werkzeugen.
Projektmodus und uv pip nicht vermischen
Angenommen, Dein uv-Projekt besitzt:
dependencies = []
und Du installierst manuell:
uv pip install rich
in .venv.
Dann funktioniert:
import rich
zunächst vielleicht.
Das Projekt weiß aber nichts davon.
Ein späteres:
uv sync
kann Rich deshalb wieder entfernen.
Für Projektabhängigkeiten verwendest Du:
uv add rich
und nicht:
uv pip install rich
uv pip ist eine bewusst niedrigere Schnittstelle für Environments, keine
zweite Art, uv-Projekte zu pflegen.
Python-Versionen mit uv verwalten
uv kann Python selbst herunterladen und verwalten.
Explizit geht das beispielsweise mit:
uv python install 3.14
Damit installiert uv die neueste unterstützte Patch-Version dieser 3.14-Linie.
Eine konkrete Version ist ebenfalls möglich:
uv python install 3.14.7
uv kann unter anderem CPython und PyPy verwalten.
Die verfügbaren Managed-Python-Versionen gehören zur jeweiligen uv-Version. Wenn eine ganz neue Python-Version nicht gefunden wird, kann deshalb auch ein:
uv self update
notwendig sein.
Oft musst Du Python gar nicht vorher installieren
uv kann fehlende Python-Versionen standardmäßig automatisch herunterladen.
Wenn ein Projekt beispielsweise Python 3.14 verlangt und kein geeigneter lokaler Interpreter vorhanden ist, kann uv bei einem normalen Projektkommando den passenden Managed Interpreter holen.
Ein vorheriges:
uv python install ...
ist also nicht zwingend für jeden Workflow notwendig.
Das explizite Kommando ist praktisch, wenn Du Interpreter bewusst im Voraus bereitstellen möchtest.
.python-version
Mit:
uv python pin 3.14
erzeugt uv:
.python-version
mit dem gewünschten Interpreter Request.
Diese Datei sagt:
Verwende für dieses Projekt standardmäßig Python 3.14.
uv init erzeugt eine solche Datei inzwischen standardmäßig ebenfalls.
.python-version und requires-python
Diese beiden Angaben werden gerne verwechselt.
In:
[project]
requires-python = ">=3.12"
steht:
Welche Python-Versionen unterstützt mein Projekt?
In:
.python-version
kann dagegen beispielsweise stehen:
3.14
Das bedeutet:
Welchen Interpreter soll uv für diesen Checkout standardmäßig benutzen?
Ein Projekt kann also völlig sinnvoll:
requires-python = ">=3.12"
angeben und lokal:
3.14
verwenden.
Python 3.14 erfüllt schließlich die Projektanforderung.
Python für einen einzelnen Aufruf wählen
Auch ohne Pin kannst Du direkt einen Interpreter anfordern:
uv run --python 3.13 python --version
Oder:
uv run --python 3.14 python main.py
Fehlt der passende Managed Interpreter, kann uv ihn automatisch herunterladen.
Das ist besonders praktisch in Tests oder CI, wenn Du denselben Code mit mehreren Python-Versionen ausführen möchtest.
Mehrere Python-Versionen installieren
uv kann mehrere Versionen in einem Aufruf installieren:
uv python install 3.12 3.13 3.14
Ein Projekt, das bewusst mit mehreren Interpretern arbeitet, kann außerdem eine:
.python-versions
verwenden.
Das ist beispielsweise für Test-Matrizen interessant.
Für einen normalen Anwendungs-Checkout reicht meistens eine
.python-version.
Tools einmalig mit uvx ausführen
Ein weiteres uv-Kommando wirst Du schnell häufig benutzen:
uvx
Es ist ein Alias für:
uv tool run
Damit kannst Du ein Python-Kommandozeilen-Tool isoliert vom Projekt ausführen.
Zum Beispiel:
uvx ruff check .
uv stellt Ruff dafür in einem eigenen Tool-Environment bereit.
Das Environment wird cached, sodass es nicht bei jedem Aufruf komplett neu heruntergeladen werden muss.
Das Tool wird aber nicht zu einer Dependency Deines Projekts.
Weitere Beispiele:
uvx pycowsay "Hallo Dungeon"
uvx httpie --version
Eine bestimmte Tool-Version verwenden
Für einen spontanen Lauf kannst Du eine Version festlegen:
uvx ruff@0.12.0 check .
Oder ausdrücklich die aktuellste verfügbare:
uvx ruff@latest check .
Für ein Projekt ist so ein spontanes:
uvx ruff@latest
nicht besonders reproduzierbar.
Dort ist:
uv add --dev ruff
uv run ruff check .
normalerweise die bessere Wahl.
uvx oder uv run?
Die beiden Befehle sehen ähnlich aus, verfolgen aber unterschiedliche Ziele.
uvx ruff check .
führt Ruff isoliert vom Projekt aus.
uv run ruff check .
verwendet Ruff aus dem Projekt-Environment.
Für:
pytest
ruff
mypy
als festes Development Tool eines Projekts würde ich deshalb:
uv add --dev ...
uv run ...
verwenden.
Für ein Werkzeug, das Du nur kurz ausprobieren möchtest:
uvx ...
Tools dauerhaft installieren
Manche Programme möchtest Du unabhängig von irgendeinem Projekt dauerhaft als Shell-Kommando benutzen.
Dann:
uv tool install ruff
Anschließend:
ruff --version
Das Tool wird weiterhin in einem isolierten Environment verwaltet, sein Executable landet aber in einem Verzeichnis für user-weite Commands.
Falls dieses Verzeichnis noch nicht in Deinem PATH steht:
uv tool update-shell
Aktualisieren:
uv tool upgrade ruff
Alle installierten Tools aktualisieren:
uv tool upgrade --all
Entfernen:
uv tool uninstall ruff
Projekttool oder globales Tool?
Für Ruff gibt es also gleich mehrere Varianten:
uvx ruff check .
für einen isolierten spontanen Aufruf.
uv tool install ruff
für ein persönliches global verfügbares Tool.
Und:
uv add --dev ruff
uv run ruff check .
für eine reproduzierbare Projektabhängigkeit.
Für ein Team-Projekt ist die letzte Variante meistens die sinnvollste.
Dann entscheidet das Repository über die Ruff-Version und nicht die zufällige Installation eines Entwicklers.
Einzelne Skripte mit Abhängigkeiten
Für ein kleines Skript ist ein komplettes Projekt manchmal übertrieben.
PEP 723 definiert dafür Inline Script Metadata.
Eine Datei skript.py kann beispielsweise enthalten:
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "rich",
# ]
# ///
from rich import print
print("[bold green]Läuft ohne manuell angelegtes venv.[/bold green]")
Start:
uv run skript.py
uv liest den Metadatenblock, stellt ein passendes isoliertes Environment bereit und startet das Skript darin.
Das ist besonders angenehm für:
kleine Admin-Tools
einzelne Utility-Skripte
Beispiele mit Dependencies
Skripte, die Du weitergeben möchtest
PEP-723-Skripte sind vom Projekt isoliert
Liegt so ein Skript innerhalb eines uv-Projekts, verwendet es aufgrund seiner Inline-Metadaten trotzdem ein eigenes Script-Environment.
Das verhindert, dass zufällig installierte Projektabhängigkeiten das Verhalten des Skripts verändern.
Willst Du ein normales Skript dagegen einfach mit den Dependencies des Projekts ausführen, braucht es keinen PEP-723-Block:
uv run python skript.py
oder:
uv run skript.py
Der Metadatenblock hat also eine echte Bedeutung und ist nicht nur eine andere
Schreibweise für pyproject.toml.
Script Metadata nicht von Hand schreiben
uv kann ein Skript initialisieren:
uv init --script skript.py --python 3.14
Und eine Dependency hinzufügen:
uv add --script skript.py rich
uv ergänzt den Metadatenblock dann selbst.
Eine Dependency entfernen:
uv remove --script skript.py rich
Damit lässt sich auch ein einzelnes Skript sauber über die uv-CLI verwalten.
Auch ein Skript kann ein Lockfile bekommen
PEP-723-Metadaten enthalten normalerweise Dependency Constraints, aber nicht zwingend einen vollständig eingefrorenen Dependency Graph.
Wenn ein einzelnes Skript reproduzierbar gelockt werden soll:
uv lock --script skript.py
uv erzeugt daneben beispielsweise:
skript.py.lock
Spätere Script-Kommandos können dieses Lockfile wiederverwenden.
Für ein fünfzeiliges Wegwerfskript braucht das niemand.
Für ein eigenständiges Utility, das lange unverändert reproduzierbar laufen soll, kann es dagegen sinnvoll sein.
Ein Blick auf eine typische pyproject.toml
Nach ein paar Kommandos könnte sie beispielsweise so aussehen:
[project]
name = "mein-projekt"
version = "0.1.0"
description = "Ein kleines Python-Projekt"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"rich>=...",
]
[dependency-groups]
dev = [
"pytest>=...",
"ruff>=...",
]
Dazu gehören:
.python-version
uv.lock
.venv/
mit unterschiedlichen Aufgaben:
pyproject.toml Projektanforderungen
.python-version gewünschter lokaler Interpreter
uv.lock universell aufgelöste Dependencies
.venv/ daraus erzeugtes lokales Environment
Nur .venv ist problemlos wegwerfbar.
Der Dungeon mit uv
Unser Dungeon benötigt bisher ausschließlich die Standardbibliothek.
Eine externe Runtime Dependency haben wir also gar nicht.
Trotzdem können wir ihn mit uv verwalten.
Für unser einfaches spiel.py-Projekt:
cd dungeon-projekt
uv init --no-package
Liegt dort schon eine pyproject.toml, bricht uv init mit einem Fehler ab.
Die zusätzlich erzeugte main.py kannst Du löschen, oder Du nimmst gleich
uv init --bare.
Ruff hinzufügen:
uv add --dev ruff
Dann:
uv run ruff check .
uv run ruff format .
uv run python spiel.py
Damit ist die Entwicklungsumgebung reproduzierbar, obwohl das Spiel selbst keine externe Library benötigt.
uv in der CI
Eine einfache CI könnte so aussehen:
uv sync --locked
uv run --locked ruff check .
uv run --locked ruff format --check .
uv run --locked pytest
--locked verhindert dabei, dass die CI eine veraltete Lockdatei einfach
aktualisiert.
Wenn:
pyproject.toml
und:
uv.lock
nicht zusammenpassen, soll der Build scheitern.
Die Lösung ist dann nicht:
CI hat eben eine neue Lockdatei erzeugt.
sondern lokal:
uv lock
beziehungsweise die entsprechende Dependency-Änderung sauber vorzunehmen und das neue Lockfile zu committen.
requirements.txt exportieren
Nicht jedes Deployment- oder Fremdsystem versteht uv.lock.
uv kann deshalb klassische Requirements exportieren:
uv export --format requirements.txt > requirements.txt
Die requirements.txt ist dann ein Export des uv-Projekts.
Die maßgebliche Quelle bleibt:
pyproject.toml
uv.lock
Du solltest nicht parallel anfangen, requirements.txt und pyproject.toml
unabhängig voneinander von Hand zu pflegen.
uv kann außerdem in andere Formate exportieren, darunter pylock.toml nach
PEP 751 und CycloneDX-SBOMs.
Für den normalen Einstieg reicht requirements.txt.
Ein bestehendes requirements-Projekt übernehmen
uv kann auch beim Umstieg helfen.
Besitzt Du beispielsweise:
requirements.in
kannst Du die Anforderungen in ein uv-Projekt übernehmen:
uv add -r requirements.in
Bei einer Migration sollte man trotzdem nicht blind alle alten Dateien anschließend löschen.
Prüfe vorher:
Welche Datei ist tatsächlich die Quelle der Dependencies?
Gibt es Extras?
Gibt es private Package Indexes?
Gibt es Git-Dependencies?
Benutzt die CI pip-tools?
Wie wird deployed?
Erst wenn der uv-Workflow vollständig funktioniert, können alte Konfigurationen verschwinden.
uv ersetzt Poetry, Hatch und PDM nicht automatisch
uv kann viele Aufgaben übernehmen, die auch andere moderne Python-Tools abdecken.
Das bedeutet nicht:
Jedes bestehende Poetry-Projekt muss sofort auf uv umgestellt werden.
Wenn ein Projekt stabil läuft, ist eine Migration eine technische Änderung mit Kosten und Risiko.
Für ein neues Projekt würde ich uv heute sehr ernsthaft in Betracht ziehen.
Für ein bestehendes Projekt würde ich zuerst fragen:
Welches konkrete Problem löst die Migration?
Nur „das neue Tool ist schneller“ ist bei einem funktionierenden Repository nicht immer ein ausreichender Grund.
uv ist trotz 0.x nicht experimentell
Beim Blick auf:
uv --version
fällt auf, dass uv weiterhin eine Versionsnummer unter 1.0 verwendet.
Daraus sollte man nicht ableiten, dass das Programm nur ein Experiment wäre.
Astral bezeichnet uv ausdrücklich als stabile, produktiv eingesetzte Software und verwendet ein eigenes Versionierungsschema.
Dabei gilt grob:
Minor Release darf Breaking Changes enthalten
Patch Release Bugfixes und nicht brechende Erweiterungen
Ein Sprung beispielsweise von:
0.12.x
auf:
0.13.x
verdient deshalb mehr Aufmerksamkeit als bei einem klassischen Semantic-Versioning-Verständnis.
In CI oder reproduzierbaren Build-Umgebungen ist es entsprechend sinnvoll, auch die verwendete uv-Version bewusst zu verwalten.
Wann uv besonders angenehm ist
uv spielt seine Stärken vor allem aus, wenn mehrere dieser Dinge zusammenkommen:
- Projektabhängigkeiten
- Development Dependencies
- reproduzierbare Environments
- CI
- mehrere Python-Versionen
- Kommandozeilen-Tools
- häufig neu erzeugte Environments
- einzelne Skripte mit Dependencies
Der Alltag wird dann sehr einheitlich:
uv add ...
uv remove ...
uv run ...
uv sync
Dazu kommen bei Bedarf:
uv python ...
uv tool ...
uvx ...
Wann venv und pip völlig reichen
Für:
print("Hallo Welt")
brauchst Du keinen Projektmanager.
Auch ein kleines Tutorial kann bewusst einfach bei:
python -m venv .venv
python -m pip install paket
bleiben.
Dass uv viele Aufgaben bündelt, macht pip und venv nicht falsch.
Du solltest sogar verstanden haben, was ein:
Interpreter
Virtual Environment
Package
Dependency
Lockfile
ist, bevor Du die Automatisierung darüber wirklich einordnen kannst.
uv macht diese Konzepte bequemer. Es zaubert sie nicht weg.
Häufige Stolperfallen
-
Den aktuellen
uv init-Default mit einem einfachenmain.py-Projekt verwechseln:uv initerzeugt inzwischen standardmäßig eine installierbare Application mitsrc-Layout. Für einen flachen Lerncode gibt esuv init --no-package. -
.venvins Git committen: Die Umgebung ist ein lokales Build-Produkt.pyproject.tomlund normalerweiseuv.lockgehören ins Repository. -
uv.lockignorieren: Damit wirfst Du einen wichtigen Teil des reproduzierbaren Projekt-Workflows weg. -
uv.lockfür eine Liste mit genau einer Version pro Paket halten: Es ist universal und kann unterschiedliche Varianten für Python-Versionen oder Plattformen enthalten. -
uv addunduv pip installgleichsetzen:uv addverändert das Projekt.uv pip installverändert ein Environment. -
Mit
uv pip installzusätzliche Pakete in.venvlegen und erwarten, dass sie dort bleiben: Ein späteres exaktesuv synckann sie wieder entfernen. -
uv syncfür zwingend vor jedemuv runhalten:uv runprüft und synchronisiert das Projekt bei Bedarf selbst. -
--lockedmit--frozenverwechseln:--lockedprüft die Aktualität des Lockfiles;--frozenüberspringt diese Prüfung. -
Bei jedem
uv syncdie neuesten Pakete erwarten: Ein vorhandenes Lockfile hält die gewählten Versionen fest. Für Updates gibt es unter anderemuv lock --upgrade. -
.python-versionundrequires-pythonverwechseln: Das eine wählt den Interpreter für den Checkout, das andere beschreibt die unterstützten Python-Versionen des Projekts. -
Python immer erst manuell mit
uv python installinstallieren: uv kann fehlende Managed Python-Versionen normalerweise bei Bedarf selbst laden. -
uvxfür ein festes Projekttool verwenden: Für Ruff oder pytest im Team ist eine Development Dependency meist reproduzierbarer. -
Ein PEP-723-Skript für ein normales Projektskript halten: Mit Inline Metadata erhält es ein eigenes isoliertes Script-Environment.
-
Ein Script Lockfile automatisch erwarten: Ein PEP-723-Skript wird mit
uv lock --script ...ausdrücklich gelockt. -
Eine exportierte
requirements.txtparallel manuell pflegen: Dann entstehen zwei Wahrheiten. Sie sollte bei einem uv-Projekt aus der eigentlichen Projektdefinition erzeugt werden. -
Eine Migration nur wegen Geschwindigkeit durchführen: Bestehende Deployment-, CI- und Dependency-Workflows sollten zuerst verstanden werden.
Kompakte Übersicht
| Aufgabe | Klassisch | Mit uv |
|---|---|---|
| einfaches Projekt anlegen | Dateien selbst anlegen | uv init --no-package |
| installierbare App anlegen | Packaging selbst konfigurieren | uv init |
| venv anlegen | python -m venv .venv |
uv venv |
| Projekt-Dependency hinzufügen | pip + Metadaten selbst pflegen |
uv add rich |
| Dependency entfernen | Metadaten + pip uninstall |
uv remove rich |
| Projekt ausführen | venv aktivieren, dann python ... |
uv run python ... |
| Environment herstellen | pip install -r ... |
uv sync |
| Lockfile erzeugen | zusätzliches Tool | automatisch / uv lock |
| alle Dependencies upgraden | abhängig vom Workflow | uv lock --upgrade |
| Python installieren | externes Tool | uv python install 3.14 |
| Python für Projekt wählen | z. B. pyenv | uv python pin 3.14 |
| Dev Dependency | separates Requirements-Setup | uv add --dev ruff |
| Dependency Tree | zusätzliches Tool | uv tree |
| Tool einmalig ausführen | pipx run ... |
uvx ... |
| Tool dauerhaft installieren | pipx install ... |
uv tool install ... |
| einmalige Zusatzdependency | temporäres venv | uv run --with ... |
| Skript mit Dependencies | eigenes venv | PEP 723 + uv run skript.py |
| Script locken | Zusatzlösung nötig | uv lock --script skript.py |
| Requirements exportieren | abhängig vom Tool | uv export --format requirements.txt |
Übungen
1. Ein einfaches uv-Projekt anlegen
Lege ein Projekt mit main.py an, füge rich hinzu und starte das Programm.
Lösung
uv init --no-package mein-projekt
cd mein-projekt
uv add rich
from rich import print
print("[bold green]Hallo uv![/bold green]")
uv run main.py
2. Ruff als Development Dependency hinzufügen
Füge Ruff als Entwicklungsabhängigkeit hinzu und starte den Linter.
Lösung
uv add --dev ruff
uv run ruff check .
uv run ruff format .
3. Eine Python-Version pinnen
Pinne das Projekt auf Python 3.14 und prüfe den verwendeten Interpreter.
Lösung
uv python pin 3.14
uv run python --version
.python-version
4. Ruff einmalig mit uvx starten
Führe Ruff aus, ohne es dem Projekt als Dependency hinzuzufügen.
Lösung
uvx ruff check .
uv add --dev ruff
uv run ruff check .
5. Ein PEP-723-Skript erstellen
Lege ein einzelnes Skript mit rich als Dependency an.
Lösung
uv init --script skript.py --python 3.12
uv add --script skript.py rich
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "rich",
# ]
# ///
from rich import print
print("[bold cyan]Single-File-Skript mit Dependency[/bold cyan]")
uv run skript.py
6. Das Lockfile in der CI prüfen
Synchronisiere das Projekt so, dass eine veraltete uv.lock nicht automatisch
korrigiert wird.
Lösung
uv sync --locked
Weiterlesen
- Python lernen, Teil 10: Module und Projektstruktur
- Ruff: Linter und Formatter in einem
- uv-Dokumentation
- uv installieren
- uv-Projekte erstellen
- uv: Projektstruktur und Dateien
- uv: Locking und Syncing
- uv: Dependencies verwalten
- uv: Python-Versionen
- uv: Tools verwenden
- uv: Skripte ausführen
- uv: pip-kompatible Schnittstelle
- uv: Versionierung
- PEP 723: Inline script metadata
- PEP 735: Dependency Groups in pyproject.toml
0 Kommentare
Noch keine Kommentare. Sei der/die Erste!
Anmelden um einen Kommentar zu hinterlassen.