Zum Inhalt springen

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:

  • venv erzeugt die virtuelle Umgebung,
  • pip installiert Pakete,
  • ein zusätzliches Tool erzeugt reproduzierbare Lockfiles,
  • pipx kümmert sich um isolierte Kommandozeilen-Tools,
  • pyenv oder ä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:

  1. ergänzt die Abhängigkeit in pyproject.toml,
  2. löst ihre Dependencies auf,
  3. erzeugt oder aktualisiert uv.lock,
  4. erzeugt bei Bedarf .venv,
  5. 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.lock enthä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 einfachen main.py-Projekt verwechseln: uv init erzeugt inzwischen standardmäßig eine installierbare Application mit src-Layout. Für einen flachen Lerncode gibt es uv init --no-package.

  • .venv ins Git committen: Die Umgebung ist ein lokales Build-Produkt. pyproject.toml und normalerweise uv.lock gehören ins Repository.

  • uv.lock ignorieren: Damit wirfst Du einen wichtigen Teil des reproduzierbaren Projekt-Workflows weg.

  • uv.lock fü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 add und uv pip install gleichsetzen: uv add verändert das Projekt. uv pip install verändert ein Environment.

  • Mit uv pip install zusätzliche Pakete in .venv legen und erwarten, dass sie dort bleiben: Ein späteres exaktes uv sync kann sie wieder entfernen.

  • uv sync für zwingend vor jedem uv run halten: uv run prüft und synchronisiert das Projekt bei Bedarf selbst.

  • --locked mit --frozen verwechseln: --locked prüft die Aktualität des Lockfiles; --frozen überspringt diese Prüfung.

  • Bei jedem uv sync die neuesten Pakete erwarten: Ein vorhandenes Lockfile hält die gewählten Versionen fest. Für Updates gibt es unter anderem uv lock --upgrade.

  • .python-version und requires-python verwechseln: 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 install installieren: uv kann fehlende Managed Python-Versionen normalerweise bei Bedarf selbst laden.

  • uvx fü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.txt parallel 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
In `main.py`:
from rich import print

print("[bold green]Hallo uv![/bold green]")
Start:
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 .
Ruff wird damit Teil der Development Dependencies des Projekts.

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
Falls kein passender Interpreter vorhanden ist und automatische Python-Downloads aktiviert sind, kann uv ihn selbst herunterladen. Die gewünschte Version steht anschließend in:
.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 .
Für ein dauerhaftes Team-Projekt wäre dagegen:
uv add --dev ruff
uv run ruff check .
meist passender.

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
Der relevante Teil von `skript.py` enthält danach Inline Metadata:
# /// script
# requires-python = ">=3.12"
# dependencies = [
#     "rich",
# ]
# ///
Ergänze:
from rich import print

print("[bold cyan]Single-File-Skript mit Dependency[/bold cyan]")
Start:
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
Passt das Lockfile nicht mehr zur Projektdefinition, schlägt der Befehl fehl.

Weiterlesen

0 Kommentare

Noch keine Kommentare. Sei der/die Erste!