uv, src-Layout und ruff

Track KI · M0 Python produktiv, Bausteine 01, 02 und 07 · ca. 60 Min. (plus optional 20 Min. lokal)

Worum es geht

Bevor du KI-Anwendungen baust, brauchst du ein Python-Projekt, das bei jedem anderen genauso läuft wie bei dir. Dafür gibt es drei Werkzeuge, die zusammen M0 (Python produktiv) eröffnen: uv verwaltet Python-Version, Abhängigkeiten und Umgebung, das src-Layout (src layout) sorgt dafür, dass deine Tests das installierte Paket prüfen und nicht zufällig den lokalen Ordner, und ruff findet Fehler und formatiert deinen Code.

Das Ziel von M0 laut Lernplan: Dein Repo läuft bei jemand anderem mit uv sync, die Tests sind grün, ruff ist sauber. Diese Lektion legt den ersten Teil davon (Projekt, Lockfile, Layout, ruff). Type Hints, Pydantic, async und pytest folgen in den nächsten Lektionen.

Die Werkzeuge selbst laufen nicht im Browser (Pyodide kann kein uv und kein ruff starten). Darum trainierst du hier die Konzepte mit kleinen Simulatoren: eine pyproject.toml mit tomllib lesen, ein Lockfile gegen die Abhängigkeiten prüfen, nachrechnen, welchen Ordner Python beim Import findet, und Code reparieren, den ruff beanstandet. Die echten Befehle (uv sync, ruff check) übst du in einer lokalen Übung im lernlabor/.

Von JS/TS her gedacht

Fast alles hat ein Gegenstück in der Node-Welt:

Aufgabe JS/TS Python mit uv
Projekt anlegen npm init uv init
Abhängigkeiten stehen in package.json pyproject.toml
Genaue Versionen und Hashes package-lock.json uv.lock
Installiert wird nach node_modules/ .venv/
Paket hinzufügen npm install fastapi uv add fastapi
Nur Dev-Werkzeug hinzufügen npm install -D vitest uv add --dev pytest
Umgebung exakt aus dem Lockfile herstellen npm ci uv sync
Befehl in der Projektumgebung npx vitest oder npm run test uv run pytest
Linter und Formatter ESLint plus Prettier (zwei Werkzeuge) ruff check und ruff format (eines)

(npm ci und uv sync verhalten sich ähnlich, aber nicht identisch. Die Tabelle ist eine Orientierung, kein 1:1-Beweis, allgemeines Fachwissen.)

Ein Unterschied wird dich beißen: Node sucht Module immer in node_modules/ über dem Ordner der importierenden Datei. Python sucht dagegen in einer Liste von Ordnern namens sys.path, und der aktuelle Ordner steht oft ganz vorn (zum Beispiel bei python -m pytest, allgemeines Fachwissen). Daran hängt das ganze Thema src-Layout.

Konzept

Schritt 1: uv, ein Werkzeug statt vier

Vorher gab es pip, venv, requirements.txt und oft noch poetry oder pip-tools obendrauf. Der klassische Satz bei kaputten Setups ist “bei mir läuft’s”. uv ist in Rust geschrieben, sehr schnell, und deckt Projektanlage, Abhängigkeiten, Lockfile und sogar die Python-Version selbst ab (Quelle).

Die vier Befehle, die du brauchst:

uv init mein-projekt          # Projekt anlegen
cd mein-projekt
uv add fastapi pydantic       # installieren und Version plus Hash ins Lockfile schreiben
uv add --dev pytest ruff      # dasselbe für Werkzeuge, die nur du beim Entwickeln brauchst
uv sync                       # Umgebung exakt aus uv.lock herstellen
uv sync --no-dev              # dasselbe ohne die Dev-Gruppe (z. B. für ein Produktions-Image)
uv run pytest                 # Befehl in der Projektumgebung, ohne selbst zu aktivieren

Was uv init anlegt, siehst du, wenn man es ausführt. Lokal ausgeführt mit uv 0.12.10 (uv init demo-app, dann uv sync, eine Projektanlage ohne Abhängigkeiten braucht kein Netzwerk):

demo-app/
  pyproject.toml
  src/demo_app/__init__.py
  README.md   .python-version   .gitignore   (und ein neues Git-Repo)
Using CPython 3.12.9
Creating virtual environment at: .venv
Resolved 1 package in 3ms
 + demo-app==0.1.0 (from file:///.../demo-app)

Zwei Dinge fallen auf. Erstens: Diese uv-Version legt von selbst ein src-Layout an (Schritt 4). Das war bei älteren Versionen anders (bitte prüfen, dein uv kann abweichen). Zweitens: Die von uv erzeugte .gitignore enthält .venv, aber nicht uv.lock. Das ist Absicht. Das Lockfile gehört ins Repo.

Schritt 2: pyproject.toml lesen

pyproject.toml ist das package.json von Python: ein TOML-Dokument (Tabellen in [eckigen Klammern], Listen in [...], Strings in Anführungszeichen). Die Standardbibliothek liest es seit Python 3.11 mit tomllib. Das Ergebnis ist ein normales dict:

Die Zahlen sind aus dem echten Lernlabor-Projekt übernommen (fastapi>=0.141.1 und so weiter). Merke: [project] dependencies sind die Laufzeit-Abhängigkeiten (ohne sie läuft dein Programm nicht). uv add --dev landet dagegen in [dependency-groups] unter dev (Werkzeuge für dich: Tests, Linter). Die Gruppen sind getrennte Listen. Ein Eintrag wie "fastapi>=0.141.1" ist ein Versions-Specifier: Name plus Bedingung. Übung 1 trainiert genau das Lesen.

Auch die Konfiguration von ruff und pytest liegt in dieser Datei, in Tabellen wie [tool.ruff]. Ein Ort für alles statt drei verschiedener Dateien (Quelle). Dazu mehr in Schritt 5.

Schritt 3: Lockfile, die Garantie

In pyproject.toml stehen Bedingungen (>=0.141.1). In uv.lock stehen die genauen Ergebnisse: jede Version, auch jede indirekte Abhängigkeit, jeweils mit Hash. So sieht ein Eintrag aus (gekürzt, aus dem echten lernlabor/uv.lock):

[[package]]
name = "annotated-doc"
version = "0.0.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = ".../annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce0..." }

uv sync liest dieses Lockfile und stellt exakt dieselbe Umgebung wieder her, egal auf welchem Rechner. Fehlt das Lockfile im Repo, löst uv bei jedem Lauf neu auf, und ein Paket, das über Nacht eine neue Version bekommen hat, bringt deine Tests auf einem fremden Rechner zum Kippen. Darum die Stolperfalle der Quelle: uv.lock gehört ins Repo, nicht in .gitignore.

Ein kleines Modell dafür. Der Name eines Pakets wird dabei normalisiert: Kleinbuchstaben, und _, . und - zählen gleich (annotated_doc und annotated-doc sind dasselbe Paket: Der Name im Lockfile hat einen Bindestrich, der Dateiname in der URL einen Unterstrich). Zwei Hilfsfunktionen: zerlege macht aus einem Specifier ein Paar (Name, Mindestversion), version_tuple macht aus "0.141.1" das Tupel (0, 141, 1).

Warum Tupel? Versionen sind Zahlenfolgen, keine Texte. Der Text "0.141.1" ist alphabetisch kleiner als "0.9", die Version 0.141.1 ist aber neuer als 0.9:

Jetzt der Abgleich, den uv sync im Kern macht. lock und installiert sind dicts Name -> Version. Die Funktion liefert die Schritte, die nötig sind, damit die Umgebung dem Lockfile entspricht, im Stil der uv-Ausgabe (+ installieren, - entfernen):

Drei Fälle, ein Prinzip: Am Ende gilt installiert == lock. Genau das meint die Übung der Quelle: .venv komplett löschen und nur mit uv sync wiederherstellen. In Übung 2 prüfst du die andere Richtung: Passt das Lockfile noch zu den Abhängigkeiten in pyproject.toml? Ein veraltetes Lockfile (jemand hat in pyproject.toml eine Abhängigkeit ergänzt, aber uv.lock nicht eingecheckt) fällt je nach Flag unterschiedlich auf. Lokal ausgeführt mit uv 0.12.10, in einem Wegwerf-Projekt mit der neuen Abhängigkeit idna in pyproject.toml und altem Lockfile:

$ uv sync --locked
error: The lockfile at `uv.lock` needs to be updated, but `--locked` was provided.

$ uv sync --frozen
(läuft durch: installiert, was im alten Lockfile steht, und sieht die neue Abhängigkeit gar nicht)

--locked bricht ab, wenn uv.lock nicht zu pyproject.toml passt (gut für die CI, denn der Fehler fällt sofort auf). --frozen nimmt das Lockfile, wie es ist, ohne zu prüfen (schnell, aber blind). Ohne eines der beiden Flags aktualisiert uv sync das Lockfile von selbst. Auch --no-dev aus Schritt 1 hast du jetzt gesehen: Es lässt die Dev-Gruppe weg. Die Details zu allen Flags sind eine Frage an uv sync --help (bitte prüfen, dein uv kann abweichen).

Schritt 4: src-Layout, ein Importtest

Die Struktur (Quelle):

mein-projekt/
  pyproject.toml
  src/
    mein_paket/
      __init__.py
      main.py
  tests/
    test_main.py

Das flache Layout (flat layout) hätte den Ordner mein_paket/ direkt neben pyproject.toml. Dann liegt er im aktuellen Ordner, und der steht beim Testen oft vorn auf sys.path. Der Import klappt, auch wenn das Paket nie installiert wurde. Das verschleiert Fehler (fehlende Dateien im Paket, falsche Abhängigkeiten), die erst beim Kunden auffallen. Im src-Layout liegt der Ordner nicht im aktuellen Ordner, Python findet ihn nur, wenn das Paket wirklich installiert ist (mit uv sync, Quelle: “erzwingt eine echte Installation”).

Ein Simulator. sys_path ist die Liste der Ordner, dateisystem sagt, welche Namen in welchem Ordner liegen. Python nimmt den ersten Ordner auf der Liste, der den Namen enthält:

A findet das Paket im aktuellen Ordner, obwohl nichts installiert ist. B scheitert ehrlich mit ImportError (hier None). Bei C zeigt der Eintrag src auf dem Pfad die Installation: uv installiert dein Projekt per Voreinstellung editierbar (editable), dein Quellcode in src/ ist dann direkt die installierte Fassung und ein Pfadeintrag zeigt dorthin (allgemeines uv-Wissen, bitte prüfen).

Dasselbe echt, lokal ausgeführt (Python 3.13, zwei winzige Projekte in Temp-Ordnern, ohne Installation):

flach:   python3 -c "import mein_paket"  ->  funktioniert, Datei liegt im Projektordner
src:     python3 -c "import mein_paket"  ->  ModuleNotFoundError: No module named 'mein_paket'

Der Merksatz der Quelle dazu: Wenn import mein_paket ohne vorheriges uv sync schon irgendwie funktioniert, stimmt am Layout etwas nicht.

Noch ein Hinweis, damit es nicht unfair wird: Ob der aktuelle Ordner vorn auf sys.path steht, hängt vom Aufruf ab (python -m pytest, ein Skript im Projektordner, eine interaktive Shell). Das Layout beseitigt die Abhängigkeit vom Aufruf.

Schritt 5: ruff, Linting und Formatierung in einem

Linting findet Fehler und Stilbrüche, bevor ein Reviewer sie findet. Formatierung beendet jede Diskussion über Leerzeichen und Zeilenlänge. ruff macht beides in einem schnellen Werkzeug (Quelle). Die Regeln schaltest du in pyproject.toml ein:

[tool.ruff]
line-length = 100

[tool.ruff.lint]
select = ["E", "F", "B", "I"]

select wählt Regelfamilien per Buchstaben. Die vier hier sind (Bedeutung allgemeines ruff-Wissen):

Familie Bedeutung Beispielregel
E pycodestyle, Stilfehler E711: Vergleich mit None über ==, soll is None sein. E722: nacktes except:
F Pyflakes, echte Fehler F401: Import nie benutzt. F841: Variable zugewiesen, nie benutzt
B flake8-bugbear, Fallen B006: veränderlicher Default-Wert wie [] oder {} als Argument
I isort, Import-Reihenfolge I001: Importblock nicht sortiert

Ein kleines Beispiel, rechnung.py:

import sys
import json


def netto(betrag, steuer=0.19, gebuehren=[]):
    rest = 0
    if betrag == None:
        return 0
    return betrag / (1 + steuer)

Lokal ausgeführt mit ruff 0.16.6 (die Version aus lernlabor/uv.lock) und der Konfiguration oben:

$ ruff check --output-format concise rechnung.py
rechnung.py:1:1: I001 [*] Import block is un-sorted or un-formatted
rechnung.py:1:8: F401 [*] `sys` imported but unused
rechnung.py:2:8: F401 [*] `json` imported but unused
rechnung.py:5:42: B006 Do not use mutable data structures for argument defaults
rechnung.py:6:5: F841 Local variable `rest` is assigned to but never used
rechnung.py:7:18: E711 Comparison to `None` should be `cond is None`
Found 6 errors.
[*] 3 fixable with the `--fix` option (3 hidden fixes can be enabled with the `--unsafe-fixes` option).

Das [*] markiert, was ruff check --fix selbst reparieren darf. Danach, ebenfalls ausgeführt:

$ ruff check --fix --output-format concise rechnung.py
rechnung.py:3:42: B006 Do not use mutable data structures for argument defaults
rechnung.py:4:5: F841 Local variable `rest` is assigned to but never used
rechnung.py:5:18: E711 Comparison to `None` should be `cond is None`
Found 5 errors (2 fixed, 3 remaining).

--fix hat nur die mechanisch sicheren Fälle repariert (die nicht benutzten Imports). Den Rest (Default-Wert, ungenutzte Variable, == None) lässt es stehen, weil die Reparatur dort die Bedeutung ändern könnte. ruff format räumt danach die leeren Zeilen auf, die der entfernte Import hinterlassen hat. Die Reihenfolge im Alltag ist also: ruff check --fix, ruff format, dann den Rest von Hand.

Eingebunden als Pre-Commit-Hook läuft das bei jedem Commit automatisch (Quelle). Ein rotes ruff-Ergebnis vor dem Commit ist billiger als dieselbe Anmerkung drei Tage später im Review.

Falle

Falle 1: uv.lock in der .gitignore. Dann kann niemand prüfen, ob “läuft bei jemand anderem” stimmt (Quelle). Das wirkt harmlos, bis die CI an einem Montag rot ist, weil ein Paket über das Wochenende eine neue Version bekommen hat. Übung 5 geht tiefer (--locked, --frozen, --no-dev).

Falle 2: Flaches Layout versteckt Importfehler. Deine Tests sind grün, weil sie den Ordner neben pyproject.toml importieren. Das gebaute Paket beim Kunden enthält diese Datei vielleicht gar nicht. Übung 3 dazu.

Falle 3: Regeln stummschalten statt Ursache beheben. Ein # noqa hinter der Zeile macht ruff still, der Fehler bleibt. B006 zum Beispiel ist kein Stilproblem, sondern ein echter Bug: Der Default-Wert wird einmal erzeugt und von allen Aufrufen geteilt (das kennst du aus JS nicht, dort wird der Default bei jedem Aufruf neu ausgewertet). Übung 4 dazu.

Falle 4: Blind --fix ausführen und nicht lesen, was geändert wurde. --fix ist bei den markierten Regeln sicher, aber lies den Diff, bevor du committest.

Übungen

Übung 1: Code vorhersagen (leicht)

Lies diese pyproject.toml (neues Projekt, andere Werte als oben). Trage ein Tupel ein:

  1. Wie viele Laufzeit-Abhängigkeiten hat das Projekt ([project] dependencies)?
  2. Wie viele Einträge haben alle Dependency-Groups zusammen?
  3. Welchen Wert hat line-length (genau so, wie tomllib ihn liefert)?
  4. Wie viele Regelfamilien stehen in select?
[project]
name = "preisrechner"
version = "0.2.0"
requires-python = ">=3.12"
dependencies = [
    "httpx>=0.27",
    "pydantic>=2.0",
    "rich",
    "typer>=0.12",
]

[dependency-groups]
dev = ["pytest>=8", "ruff>=0.5"]
docs = ["mkdocs"]

[tool.ruff]
line-length = 120

[tool.ruff.lint]
select = ["E", "F", "I", "B", "UP"]

Welche Tabelle gehört zur Laufzeit, welche zu Werkzeugen? Und welchen Typ hat eine Zahl im TOML, mit und ohne Anführungszeichen? Du darfst den Text mit tomllib.loads prüfen.

antwort = (4, 3, 120, 5)
antwort

Übung 2: Selbst schreiben, Lockfile gegen Abhängigkeiten (mittel)

Schreibe veraltet(dependencies, lock). dependencies ist eine Liste von Specifiern wie in pyproject.toml, lock ein dict Name -> Version (Namen im Lockfile sind normalisiert). Die Funktion gibt die sortierte Liste der normalisierten Namen zurück, bei denen das Lockfile veraltet ist: Das Paket fehlt im Lockfile, oder die gelockte Version ist kleiner als die Mindestversion des Specifiers.

zerlege und version_tuple aus Schritt 3 stehen dir zur Verfügung. Beispiele:

  • veraltet(["fastapi>=0.141.1", "Pydantic"], {"fastapi": "0.139.0", "pydantic": "2.13.5"}) ergibt ["fastapi"]
  • veraltet(["httpx>=0.9"], {"httpx": "0.141.1"}) ergibt []
  • veraltet(["Pillow"], {}) ergibt ["pillow"]

Zwei Gründe für “veraltet”, und beide brauchen zuerst den normalisierten Namen. Wie vergleichst du zwei Versionen, ohne dass 0.141.1 kleiner als 0.9 wird?

def veraltet(dependencies, lock):
    out = []
    for spec in dependencies:
        name, mindest = zerlege(spec)
        if name not in lock or version_tuple(lock[name]) < mindest:
            out.append(name)
    return sorted(out)
veraltet

Übung 3: Code vorhersagen, welcher Ordner gewinnt (mittel)

Nutze finde_modul aus Schritt 4. Gesucht ist, aus welchem Ordner import rechnungen kommt (None heißt ImportError). Trage für jedes der fünf Szenarien den Ordner ein, als Tupel in der Reihenfolge A bis E. Achte auf die Reihenfolge der Ordner in sys_path und darauf, ob ein Ordner den Namen direkt enthält. Bei D und E ist das Paket sogar zweimal vorhanden: Was Python wirklich lädt, entscheidet die Reihenfolge.

szenarien = {
    "A flach, Ordner heißt anders":      ([".", "site-packages"],        {".": {"rechnungen_alt", "tests"}, "site-packages": {"rechnungen"}}),
    "B src, editable, src vor Rest":     (["src", ".", "site-packages"], {".": {"src", "tests"}, "src": {"rechnungen"}, "site-packages": {"rechnungen"}}),
    "C flach, Aufruf aus anderem Ordner": (["site-packages"],            {".": {"rechnungen"}, "site-packages": set()}),
    "D flach, Kopie installiert":        ([".", "site-packages"],        {".": {"rechnungen"}, "site-packages": {"rechnungen"}}),
    "E src, Kopie installiert (Wheel)":  ([".", "site-packages"],        {".": {"src"}, "src": {"rechnungen"}, "site-packages": {"rechnungen"}}),
}

Gehe sys_path von links nach rechts durch. Der erste Ordner, der den Namen direkt enthält, gewinnt. Was liegt direkt im Ordner src, und was erst eine Ebene tiefer?

antwort = ("site-packages", "src", None, ".", "site-packages")
antwort

In D gewinnt der Ordner im Projekt ("." steht vor "site-packages"): Die installierte Kopie wird nie geladen, ein Fehler im gebauten Paket bleibt unsichtbar. In E gibt es im Projektordner keinen Ordner rechnungen, also wird die installierte Kopie geladen.

Übung 4: Fehler finden, ruff-Befunde reparieren (mittel)

ruff meldet für diese Funktion Folgendes (ausgeführt mit ruff 0.16.6 und der Konfiguration select = ["E", "F", "B", "I"]):

preise.py:1:1: I001 [*] Import block is un-sorted or un-formatted
preise.py:1:8: F401 [*] `os` imported but unused
preise.py:5:32: B006 Do not use mutable data structures for argument defaults
preise.py:8:5: E722 Do not use bare `except`
preise.py:10:17: E711 Comparison to `None` should be `cond is None`

Repariere den Code so, dass ruff nichts mehr meldet und das Verhalten stimmt: lade_preise(text, standard) liest ein JSON-Objekt aus text und ergänzt standard damit. Ist der Text kein gültiges JSON (oder null), kommt standard unverändert zurück. Wird standard weggelassen, startet jeder Aufruf mit einem leeren dict. # noqa zählt nicht.

Die meisten Befunde sind mechanisch. Beim Default-Wert reicht Austauschen nicht: Wo legst du nun das leere dict an, damit jeder Aufruf ein eigenes bekommt? Und welchen Fehler wirft json.loads bei kaputtem Text?

import json


def lade_preise(text, standard=None):
    if standard is None:
        standard = {}
    try:
        daten = json.loads(text)
    except json.JSONDecodeError:
        daten = None
    if daten is None:
        return standard
    standard.update(daten)
    return standard
lade_preise

Übung 5: Multiple Choice mit Begründung (anspruchsvoll)

Euer Repo hat uv.lock eingecheckt. Ein Kollege hat lokal uv add httpx ausgeführt und danach nur pyproject.toml committet, das Lockfile blieb liegen. Die CI läuft mit uv sync, aktualisiert das Lockfile dabei still selbst und ist grün, obwohl das eingecheckte Lockfile veraltet ist. Ihr stellt zwei Anforderungen: Die CI soll in so einem Fall rot werden. Und das Produktions-Image soll ohne pytest und ruff gebaut werden, ohne dass ihr dafür eine zweite Abhängigkeitsliste pflegt. Welche Maßnahme erfüllt beides? (Die Flags kennst du aus Schritt 1 und 3.)

  • a) CI und Image mit uv sync --frozen bauen, pytest und ruff als normale Einträge unter [project] dependencies führen, damit die CI sie findet.
  • b) CI mit uv sync --locked, Image mit uv sync --locked --no-dev, pytest und ruff per uv add --dev in die Dev-Gruppe legen, nicht unter dependencies.
  • c) CI mit uv sync wie bisher, und am Ende jedes Laufs das aktualisierte Lockfile automatisch committen, damit es zu pyproject.toml passt.
  • d) CI und Image mit uv sync --frozen --no-dev bauen und vorher in der CI jedes Mal uv lock ausführen, damit das Lockfile garantiert zu pyproject.toml passt.

Trage den Buchstaben als String ein.

Prüfe jede Option an beiden Anforderungen getrennt. Was passiert bei einem veralteten Lockfile, und was landet im Image? Frag dich außerdem, ob die CI wirklich etwas prüft oder nur das Problem neu überdeckt.

antwort = "b"
antwort

Projektaufgabe und lokale Übung (Abschluss-Check von M0, Bausteine 01, 02, 07)

Jetzt mit den echten Werkzeugen, im Projekt lernlabor/. Das sind die Übungen der Quelle, zusammengefasst:

  1. Neu aufbauen (Baustein 01): Lösche .venv im Lernlabor komplett und stelle es nur mit uv sync wieder her. uv run pytest muss danach ohne weitere Schritte laufen.
  2. Layout prüfen (Baustein 02): In der Quelle sollst du model.py ins src-Layout verschieben und die Importfehler beheben. Im Lernlabor liegt es schon unter src/lernlabor/model.py (so ist das Projekt angelegt). Prüfe, dass uv run python -c "import lernlabor" klappt, und dass deine Tests from lernlabor.model import ... verwenden, nicht from model import ....
  3. ruff (Baustein 07): Aktiviere ruff in lernlabor/pyproject.toml und lass es über ein Skript laufen, das noch nie gelintet wurde. In der Quelle ist das uebung/altes_skript.py. Das ist aber bereits ruff-sauber (ausgeführt mit den Familien E, F, B, I, UP, SIM, N, es meldet nichts), die Übung würde also nichts zeigen. Darum gibt es dafür uebung/ki/ki_01_lint_beispiel.py mit absichtlich eingebauten Befunden.

Die Anleitung mit allen Schritten steht im Kopf von lernlabor/uebung/ki/ki_01_uv_sync_neu.py. Der Selbsttest am Ende prüft nur und ändert nichts. Er braucht weder Netzwerk noch einen API-Schlüssel.

Nichts mit uv add nachinstallieren: uv sync stellt die Dev-Gruppe (pytest, ruff) schon aus dem Lockfile wieder her. Und wenn ruff nur zwei Befunde per --fix behebt: Was bleibt für dich übrig, und warum?

Die Ergänzung in lernlabor/pyproject.toml:

[tool.ruff]
line-length = 100

[tool.ruff.lint]
select = ["E", "F", "B", "I"]

In ki_01_lint_beispiel.py bleibt nach --fix und format Handarbeit: anzahl entfernen (F841), except: durch except ValueError: ersetzen (E722), == None durch is None (E711), und den Default ergebnis=[] durch None plus if ergebnis is None: ergebnis = [] (B006).

Fertig, wenn:

  • rm -rf .venv && uv sync && uv run pytest läuft ohne weitere Schritte durch.
  • uv.lock liegt im Repo und steht nicht in .gitignore.
  • uv run ruff check . im Lernlabor meldet nichts, und ruff format --check . ist sauber.
  • Der Selbsttest uebung/ki/ki_01_uv_sync_neu.py zeigt “Alles erledigt.”

Selbstcheck:

Merksatz

Ein Projekt ist erst reproduzierbar, wenn Lockfile (uv.lock) im Repo liegt, das src-Layout eine echte Installation erzwingt und ruff vor dem Commit rot oder grün sagt.

Prüfstein

Eine Kollegin klont dein Repo, uv sync läuft durch, aber pytest bricht mit ModuleNotFoundError: No module named 'dein_paket' ab, obwohl es bei dir grün ist. Nenne drei Ursachen, die du in dieser Reihenfolge prüfst, und für jede den einen Befehl oder die eine Datei, die dir die Antwort zeigt.


Quelle: quellen/kursbuch-lerninhalte.md, Modul M0, Bausteine “01 uv, der Paket- und Projektmanager” (Zeilen 89 bis 135 der Datei, Baustein 02 inklusive Übung) und “07 ruff” (Zeilen 238 bis 252). Über die Quelle hinaus (allgemeines Fachwissen): Vergleichstabelle mit npm, Normalisierung von Paketnamen, sys.path und der aktuelle Ordner je nach Aufruf, Regelfamilien E, F, B, I von ruff, Verhalten von --fix und --unsafe-fixes, editierbare Installation. Ausgeführt und nicht aus der Quelle: die uv-Ausgaben (uv 0.12.10), die ruff-Ausgaben (ruff 0.16.6), der Importtest mit Python 3.13. Bitte prüfen: Verhalten von uv sync bei veraltetem Lockfile (--locked), dass uv init in deiner uv-Version ein src-Layout anlegt, und dass die Installation des eigenen Projekts standardmäßig editierbar ist. Die Simulatoren (zerlege, sync_schritte, finde_modul) sind stark vereinfachte Modelle, keine Nachbildung von uv oder dem Python-Importsystem.