5 Punkte von GN⁺ 2025-03-31 | 1 Kommentare | Auf WhatsApp teilen
  • Selbst kleine API-Testskripte erfordern wegen ihrer Abhängigkeiten eine passende Laufzeitumgebung; mit einem uv-Ausführungs-Shebang lassen sie sich jedoch so gestalten, dass sie ohne Installationsschritte direkt ausführbar sind
  • Das Beispiel jam_users.py nutzt httpx, IPython und loguru, um den /users-Endpunkt einer Go-API zu testen: Benutzer löschen, neu anlegen und anschließend in eine REPL wechseln
  • Beim bisherigen Ansatz müssen Pakete global in der System-Python-Installation installiert oder eine virtuelle Umgebung manuell vorbereitet werden, was das Teilen und erneute Ausführen von Skripten umständlich macht
  • Wenn im Header # /// script dependencies = ["ipython", "httpx", "loguru"] deklariert ist und uv run jam_users.py ausgeführt wird, übernimmt uv die isolierte Umgebung und die Installation der Abhängigkeiten
  • Ergänzt man den Shebang #!/usr/bin/env -S uv run --script sowie Ausführungsrechte, kann das Skript auf Unix-Systemen nur mit installiertem uv wie ./jam_users.py ausgeführt werden

Abhängigkeiten im Skript deklarieren

  • Das Beispiel jam_users.py bereitet Testbenutzerdaten für die lokale API http://localhost:4000/v1/users vor
    • Sendet API-Anfragen mit httpx
    • Wechselt in eine IPython-REPL, um Antworten zu prüfen und Tests fortzusetzen
    • Protokolliert Lösch- und Erstellungsvorgänge mit loguru
  • Das grundlegende Skript folgt dem Ablauf, zunächst die Benutzerliste zurückzusetzen und anschließend Testdaten wieder einzufügen
    • Ruft mit GET /v1/users die bestehende Benutzerliste ab
    • Ruft für jeden Benutzer DELETE /v1/users/{id} auf
    • Sendet die vorbereitete Benutzerliste als JSON an POST /v1/users
    • Startet danach mit IPython.embed() eine REPL
  • Für die Ausführung mit python jam_users.py müssen httpx, IPython und loguru bereits in der Laufzeitumgebung installiert sein
  • Man kann sie zwar global in der System-Python-Installation installieren oder separat eine virtuelle Umgebung erstellen, beides erfordert jedoch Vorbereitungen vor der Ausführung
    • Eine globale Installation kann die System-Python-Umgebung mit Paketen überfrachten
    • Beim Ansatz mit virtueller Umgebung müssen Erstellung, Aktivierung, Installation und Ausführung manuell verwaltet werden
    • Beide Ansätze setzen eine System-Python-Installation voraus, die mit den benötigten Paketen kompatibel ist

Ein direkt ausführbares Skript mit uv erstellen

  • Wenn das Tag # /// script von uv am Anfang des Skripts eingefügt wird, können Abhängigkeiten innerhalb der Datei deklariert werden
# /// script

# dependencies = ["ipython", "httpx", "loguru"]

# ///
  • Mit diesem Header lässt sich das Skript per uv run jam_users.py ausführen
    • uv erstellt eine isolierte virtuelle Umgebung für das Skript
    • Lädt die benötigten Abhängigkeiten herunter und installiert sie
    • Führt das Skript im Kontext dieser virtuellen Umgebung aus
  • Ein normaler Python-Shebang wie #!/usr/bin/env python kann den uv-Skriptheader nicht nutzen, da Python die Kommentare # /// script ignoriert
  • Wird der uv-Aufruf direkt in den Shebang geschrieben, kann das Skript wie eine ausführbare Datei behandelt werden
#!/usr/bin/env -S uv run --script

# /// script

# dependencies = ["ipython", "httpx", "loguru"]

# ///
  • Das Flag -S von env sorgt dafür, dass die nachfolgende Zeichenkette in separate Argumente aufgeteilt und an env übergeben wird
  • Nachdem mit chmod +x jam_users.py Ausführungsrechte vergeben wurden, kann das Skript wie folgt direkt gestartet werden
./jam_users.py
  • Auf diese Weise lässt sich das Skript auf Unix-Systemen ausführen, sofern uv installiert ist, ohne separate Installation von Abhängigkeiten oder Verwaltung einer virtuellen Umgebung
  • Beim Weitergeben komplexer Python-Skripte an andere Nutzer reduziert dies den Aufwand, lange Vorbereitungsanleitungen für das Zielsystem mitzuliefern

1 Kommentare

 
GN⁺ 2025-03-31
Meinungen auf Hacker News
  • Nicht speziell UV, sondern ganz allgemein stört mich der Ansatz, die Codeausführung über Kommentare zu steuern.
    Kommentare für Linter-Anweisungen oder Entwicklernotizen sind in Ordnung, aber wenn es um Konfiguration oder ausführungsbezogene Daten geht, halte ich eine Form wie UV_ENV = { "dependencies": { "requests": "2.32.3", "pandas": "2.2.3" } } für deutlich besser.
    Das ist gültige Python-Syntax, nutzt statt willkürlichem Kommentar-Parsing Standard-Datenstrukturen und lässt sich dadurch leicht erzeugen und validieren. Vor allem wahrt es das Prinzip, dass der Code auch dann identisch laufen sollte, wenn alle Kommentare entfernt werden.

    • Zustimmung, aber ich würde noch einen Schritt weitergehen.
      Auch der vorgeschlagene Ansatz ist zur Laufzeit nur eine magische Konstante, die nichts tut und nur per statischer Analyse geparst wird; uv könnte sie lesen, aber ein anderes Tool könnte sie als unbenutzten Code ansehen und entfernen.
      Stattdessen könnte man nach import uv direkt aufrufen, was uv tun soll, etwa uv.exec(dependencies=["clown"], python=">=3.10").
      Der erste Lauf erfolgt in irgendeiner Python-Runtime, die dieses hypothetische uv-Paket finden kann; das uv-Paket richtet dann die virtuelle Umgebung und die Python-Runtime ein und führt anschließend mit einem Flag, etwa über eine Umgebungsvariable, ein re-exec(3) aus.
      In der zweiten Runtime erkennt uv.exec das Flag und macht einfach nichts.
    • Das hat nicht uv erfunden, sondern es verwendet wie andere Tools den Standard PEP 723.
      https://peps.python.org/pep-0723/
    • Ein berechtigter Punkt, aber imperativen Code zu parsen und auszuwerten ist meiner Meinung nach deutlich schwieriger und weniger flexibel, als deklarative Daten im Sinne des Prinzips der geringsten Rechte zu verwenden.
      Außerdem verhält sich auch die Shebang-Zeile im Grunde wie ein Kommentar.
      Sie ist seit 45 Jahren so tief verankert, dass den Leuten kaum noch bewusst ist, dass sie ein Shell-Kommentar ist.
    • Die Bedingung „der Code sollte auch dann identisch laufen, wenn Kommentare entfernt werden“ gilt weiterhin.
      Wenn dieselben Abhängigkeiten installiert sind, läuft der Code identisch.
      Es ändert nicht die Bedeutung des Codes selbst, sondern die Umgebung, in der der Code ausgeführt wird; in dieser Hinsicht unterscheidet es sich nicht von einem #!/bin/bash-Kommentar am Anfang eines Shell-Skripts.
    • Vollkommen einverstanden, und ich hoffe, dass sich so eine Form letztlich standardisiert.
      Allerdings will uv vermutlich keinen Code ausführen, nur um die Abhängigkeiten zu ermitteln; daher müsste es ein sehr eingeschränktes Subset der Python-Syntax sein.
      Schon die Tatsache, dass so etwas überhaupt nötig ist, zeigt eine Schwäche der Sprache.
      Die import-Anweisung selbst sollte alle Informationen über Abhängigkeiten vermitteln können.
  • In den letzten Monaten kam dieses Thema auf HN sehr häufig auf; aktuelle Beispiele sind:
    https://news.ycombinator.com/item?id=43500124
    https://news.ycombinator.com/item?id=42463975
    Ich mag uv, aber mit der Bezeichnung self-contained habe ich aus zwei Gründen Schwierigkeiten.
    Erstens muss uv bereits installiert sein, damit man das Skript ausführen kann.
    Man könnte ein Shell-Skript bauen, das prüft, ob uv installiert ist, und es andernfalls per curlpipe installiert, aber dadurch wächst der Boilerplate erheblich, und der curlpipe-Ansatz selbst ist auch nicht gut.
    Zweitens ist es nicht wirklich self-contained, irgendwo im Home-Verzeichnis automatisch eine virtuelle Umgebung anzulegen.
    Selbst wenn man es nur einmal ausführt und das Skript danach löscht, bleibt diese virtuelle Umgebung zurück und belegt Platz; in der uv-Dokumentation habe ich keine Zusicherung gefunden, dass solche temporären virtuellen Umgebungen automatisch aufgeräumt werden.

    • Die Kritik, dass uv nötig ist und man es, falls es fehlt, manuell installieren oder curl | sh verwenden muss, ist berechtigt.
      Allerdings wird das mit der Zeit weniger problematisch, sobald Paketmanager uv in ihre Repositories aufnehmen.
      Zum Beispiel ist uv bereits in Alpine Linux und Homebrew verfügbar: https://repology.org/project/uv/versions
      Außerdem sind Inline-Skript-Metadaten ein Python-Standard.
      Wenn auf dem System kein uv vorhanden und es auch nicht paketiert ist, aber eine zum Skript passende Python-Version existiert, kann man es mit pipx ausführen: https://pipx.pypa.io/stable/examples/#pipx-run-examples
      pipx ist wesentlich breiter paketiert: https://repology.org/project/pipx/versions
    • Um solche Probleme zu lösen, habe ich docker+uv shebang etwas gewaltsam zusammengebastelt, und es funktioniert bis zu einem gewissen Grad.
      Auf beliebigen Entwicklerrechnern ist Docker möglicherweise häufiger vorhanden als uv, und da es im Originaltext um ein Firmenprojekt ging, erschien mir das plausibel.
      Allerdings wird derzeit überhaupt nicht gecacht, sodass bei jeder Ausführung Downloads anfallen; das dürfte sich wohl mit Volumes beheben lassen.
      So sieht es aus: https://hugojosefson.github.io/docker-shebang/#python
    • Normalerweise muss man erst irgendetwas installieren, um auf einem Computer ein Programm auszuführen; insofern wirkt die Installation von uv an sich nicht so schlimm.
      Wenn beim Ausführen aber irgendwelche unbekannten Dinge aus dem Internet heruntergeladen werden, ist es schwer, das self-contained zu nennen; wirklich vollständig self-contained ist eher etwas wie AppImage.
    • Stimme zu 100 % zu.
      Mit etwas wie py2exe kann man ein self-contained „Python-Skript“ erstellen.
      Für Entwickler verursacht das viele Probleme, für Nutzer werden die Probleme aber minimiert.
    • Als kleine Ergänzung: Dank der Paket-Deduplizierung von uv belegt ein virtualenv keinen zusätzlichen Platz, solange es keine einzigartigen Abhängigkeiten enthält.
  • Mit Nix verwendet man denselben Ansatz; die Shebang-Zeile sieht dann so aus:
    #! nix-shell -i python3 -p "python312.withPackages (pkgs: [ pkgs.boto3 pkgs.click ])"
    Damit braucht das System nur Nix; Python muss nicht einmal installiert sein.

    • Stimmt zwar, aber es gibt noch viele PyPI-Pakete, die nicht in nixpkgs paketiert sind, daher ist das kein so universeller Ansatz wie uv.
    • Wie im Original genau beschrieben, installiert uv Python bei Bedarf ad hoc.
    • Dieselbe Technik lässt sich auf jede Sprache anwenden.
      Das naheliegendste Beispiel ist Bash mit allen angegebenen Abhängigkeiten; ich habe mit einem Nix-Shebang auch schon schnelle Single-File-Rust-Skripte gebaut.
      https://nixos.wiki/wiki/Nix-shell_shebang
    • Die Voraussetzung, dass Nix installiert sein muss, ist eine deutlich stärkere Anforderung als die, dass uv vorhanden sein muss.
    • Ich würde gern wissen, wie man dasselbe mit dem Flake-basierten Befehl nix shell statt nix-shell macht.
  • Wie in anderen Kommentaren hängt die Behauptung „self-contained“ davon ab, dass uv installiert ist.
    Wenn man wirklich ein self-contained Python-Skript will, lohnt sich ein Blick auf den Nuitka-Compiler.
    Wir verwenden ihn problemlos in der Produktion für einen gRPC-Service, und nuitka --onefile run.py auszuführen reicht aus.
    Da es ein Compiler ist, ist das erzeugte Binary oft auch schneller als das ursprüngliche, mit PyInstaller gebündelte Python-Programm.
    Auf der GitHub-Seite des Autors steht: „Meine Lebensaufgabe ist es, bis ins hohe Alter zu versuchen, den bestmöglichen Python Compiler zu bauen.“
    https://nuitka.net/
    https://github.com/kayhayen

  • Dieses Pattern gefällt mir wirklich, aber leider habe ich es nicht sauber mit LSP zum Laufen bekommen.
    Ich nutze pyright in Helix, und selbst wenn ich den Editor mit uv run hx script.py starte, funktioniert es nicht.
    Man kann zwar so etwas wie uv run --with whatever-it-is-i-need hx script.py machen, aber das führt zunehmend zu Dopplungen.

    • Ich verwende ein selbstgebautes, hässliches uve-Skript:
      $ cat ~/.local/bin/uve
      #!/bin/bash
      temp=$(mktemp)
      uv export --script $1 --no-hashes > $temp
      uv run --with-requirements $temp vim $1
      unlink $temp
      Ich hoffe, dass Editoren bald uv python find --script unterstützen.
  • Sieht ziemlich nützlich aus.
    Ich frage mich, ob uv für die langfristige Bereitstellung Python-basierter Projekte die sicherere Wahl ist.
    Ich denke dabei an den Rug Pull mit Anaconda vor etwa fünf Jahren: Wir nutzten es für Dependency-Management, später änderten sich die Regeln, und Kundenorganisationen mit mehr als 200 Mitarbeitenden durften Anaconda nicht mehr kostenlos verwenden, sondern mussten eine kommerzielle Lizenz bezahlen.

    • uv steht unter der MIT- oder Apache-2.0-Lizenz.
      Sie können die Entwicklung einstellen oder künftige Arbeit in einen Fork mit anderer Lizenz verschieben, aber sie können die frühere Lizenz nicht rückwirkend ändern; was heute existiert, ist also als Open Source garantiert.
      Wenn man wirklich besorgt ist, kann man einen Fork erstellen und weiter synchron halten.
      Im Grunde gilt das für andere OSS-Projekte fast genauso, daher würde ich mir keine großen Sorgen machen.
      Soweit ich weiß, war conda nie Open Source und hat Binaries verteilt.
      https://github.com/astral-sh/uv?tab=readme-ov-file#license
    • Anacondas Rug Pull betraf meines Erachtens die Repository-Seite.
      Die Pakete von conda-forge kann man weiterhin kostenlos nutzen.
      uv verwendet einfach PyPI; wenn es Probleme gibt, kann man von uv zu pip, Poetry oder Ähnlichem wechseln, und die Pakete kommen weiterhin aus derselben Quelle.
    • Soweit ich es verstehe, ist Relizenzierung möglich, wenn ein Projekt ein Contributor License Agreement (CLA) hat und Beitragsurheberrechte an den Projekteigentümer übertragen werden.
      Ein solcher Eigentümer wird am Ende von der schlimmsten reichen Person übernommen, die man sich vorstellen kann.
      Ich habe die Contribution-Guides und Issues von uv überflogen, aber kein CLA gesehen; bei PyTorch stand ein CLA ganz oben im Contribution-Guide.
      Trotzdem hätte es einen Community-Fork der letzten FOSS-Version von Anaconda geben müssen.
      Bei Redis ist das passiert, und Redis verwendet ebenfalls ein CLA: https://github.com/redis/redis/blob/unstable/CONTRIBUTING.md#redis-software-grant-and-contributor-license-agreement
      Man sollte niemals ein CLA unterschreiben.
      Ich finde, man sollte lieber nur zu Copyleft-Projekten beitragen.
      Wir werden zu gut bezahlt, um kostenlos zu arbeiten.
  • Für kleine Utilities wirkt das wie eine gute Packaging-Alternative zur Containerisierung.
    Jetzt muss ich nur noch alle Kolleginnen und Kollegen überzeugen, uv zu installieren.

    • uv ist erstaunlich schnell, das wird helfen.
    • Bei uns ist der Haken, dass unser SCA-Schwachstellen-Scanner noch nicht mit uv funktioniert.
  • Sieht ähnlich aus wie bundler/inline in Ruby.
    Schön, dass es etwas Ähnliches nun auch für Python gibt; in der Praxis ist es wirklich sehr bequem.
    https://bundler.io/guides/bundler_in_a_single_file_ruby_script.html

  • Ich frage mich, ob das jemand unter Windows zum Laufen gebracht hat
    Ich wollte diesen Trick für ein Tool für einen Spiel-Mod verwenden, an dem ich arbeite, konnte den Shebang-Trick aber nicht zum Laufen bringen

  • Durch diesen Anwendungsfall habe ich uv schätzen gelernt, aber ich finde, dass es dem Zen of Python widerspricht, dass ein offizielles und sehr nützliches PEP nicht von den offiziellen Python-Tools unterstützt wird
    Aus meiner Sicht war das der erste Moment, in dem Python nicht mehr „batteries included“ war
    Jetzt habe ich auch zwei Python-Dependency-Manager auf dem System
    Ich weiß, dass es zur Python-Abhängigkeitsverwaltung viel zu sagen gibt, aber in den letzten Jahren bin ich bei Projekten mit nur einer requirements.txt mit dem Standard-pip+venv zurechtgekommen

    • Bei den Python-Build-Spezifikationen gab es so eine Entwicklung schon
      Ich meine mich zu erinnern, dass pyproject.toml vor der Bibliothek tomllib kam
      Daher musste man einige Versionen lang Module in einer Sprache spezifizieren, die Python standardmäßig nicht lesen konnte
      Das ist eine schlechtere Situation, als wenn es ein Standardverfahren gäbe, das ungenutzte Metadaten enthält
      Genau deshalb sind es schließlich Metadaten; andernfalls wäre es einfach Python-Syntax gewesen