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 aktivierenWas 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:
- Wie viele Laufzeit-Abhängigkeiten hat das Projekt (
[project] dependencies)? - Wie viele Einträge haben alle Dependency-Groups zusammen?
- Welchen Wert hat
line-length(genau so, wietomllibihn liefert)? - 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")
antwortIn 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 --frozenbauen, pytest und ruff als normale Einträge unter[project] dependenciesführen, damit die CI sie findet. - b) CI mit
uv sync --locked, Image mituv sync --locked --no-dev, pytest und ruff peruv add --devin die Dev-Gruppe legen, nicht unterdependencies. - c) CI mit
uv syncwie bisher, und am Ende jedes Laufs das aktualisierte Lockfile automatisch committen, damit es zupyproject.tomlpasst. - d) CI und Image mit
uv sync --frozen --no-devbauen und vorher in der CI jedes Maluv lockausführen, damit das Lockfile garantiert zupyproject.tomlpasst.
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"
antwortProjektaufgabe 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:
- Neu aufbauen (Baustein 01): Lösche
.venvim Lernlabor komplett und stelle es nur mituv syncwieder her.uv run pytestmuss danach ohne weitere Schritte laufen. - Layout prüfen (Baustein 02): In der Quelle sollst du
model.pyins src-Layout verschieben und die Importfehler beheben. Im Lernlabor liegt es schon untersrc/lernlabor/model.py(so ist das Projekt angelegt). Prüfe, dassuv run python -c "import lernlabor"klappt, und dass deine Testsfrom lernlabor.model import ...verwenden, nichtfrom model import .... - ruff (Baustein 07): Aktiviere ruff in
lernlabor/pyproject.tomlund lass es über ein Skript laufen, das noch nie gelintet wurde. In der Quelle ist dasuebung/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üruebung/ki/ki_01_lint_beispiel.pymit 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 pytestläuft ohne weitere Schritte durch.uv.lockliegt im Repo und steht nicht in.gitignore.uv run ruff check .im Lernlabor meldet nichts, undruff format --check .ist sauber.- Der Selbsttest
uebung/ki/ki_01_uv_sync_neu.pyzeigt “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.