Prompts fürs Code-Review: 16 fertige Prüfungen

Sechzehn Ausschnitte mit je einem echten Fehler und der Anmerkung, die ein Reviewer dazu schreiben würde. Wer seinen Code prüfen lassen will, ersetzt das Beispiel durch eigenen Code. Sprache, Fokus und Einsatzumgebung sind schon gesetzt.

Bugs und Logik

Code, der kompiliert und trotzdem lügt: den Fehler in der Logik finden, einen veränderbaren Default-Parameter entlarven, einen Zeitvergleich ohne Zone aufspüren und die Regression im Pull Request abfangen.

Veränderbarer Default-Parameter

Die klassische Python-Falle: Die Liste in der Signatur überlebt den Aufruf und sammelt fremde Daten.

Prompt

Prüfe diese Warenkorb-Funktion: Kunden finden Artikel im Korb, die sie nie hineingelegt haben.def add_item(item, cart=[]): cart.append(item) return cart

Ergebnis

Zeile 1 · kritisch · Bugcart=[] entsteht einmal beim Import des Moduls, nicht bei jedem Aufruf: Der zweite Kunde füllt weiter den Korb des ersten — bis der Prozess neu startet.Fix: cart: list | None = None und als erste Zeile im Rumpf if cart is None: cart = [].

Null als fehlender Wert

Die Wahrheitsprüfung verdeckt eine echte Null, und der Kunde mit leerem Guthaben sieht einen Platzhalter.

Prompt

Sieh dir diese Prüfung an: Unterscheidet sie wirklich einen fehlenden Wert von einer Null?def render_balance(user): balance = user.get("balance") if not balance: return "keine Daten" return format_money(balance)

Ergebnis

Zeile 3 · schwer · Bugif not balance trifft None, 0 und 0.0 gleichermaßen: Wer sein Guthaben restlos ausgegeben hat, bekommt "keine Daten" statt einer ehrlichen Null.Fix: if balance is None — das Fehlen wird explizit geprüft, die Null geht ganz normal in die Formatierung.

Zeitvergleich ohne Zone

datetime.now() liefert die lokale Serverzeit, während die Gültigkeit des Tokens in UTC gespeichert ist.

Prompt

Prüfe die Ablaufrechnung für das Token. Der Dienst läuft auf Servern in mehreren Zeitzonen.def is_expired(token): return datetime.now() > token.expires_at

Ergebnis

Zeile 2 · kritisch · Bugdatetime.now() ist naive Ortszeit, expires_at kommt in UTC aus der Datenbank: In Berlin leben Tokens zwei Stunden zu lang, und gegen einen aware-Wert scheitert der Vergleich sofort mit TypeError.Fix: datetime.now(timezone.utc) und eine Regel fürs ganze Projekt — gespeichert wird nur aware und in UTC.

Regression im Pull Request

Ein Review der Änderung statt der Datei: was mit diesem Commit in Produktion geht.

Prompt

Reviewe diesen Diff aus einem Pull Request: Was genau geht damit in Produktion?@@ -12,8 +12,7 @@ def apply_discount(order, percent): if order.paid: raise ValueError("order already paid")- if percent > 90:- raise ValueError("discount too large") order.total = order.total * (100 - percent) / 100+ order.discount_percent = percent

Ergebnis

Zeilen 15-16 · kritisch · BugDie gelöschte Prüfung war die einzige Grenze für percent: Bei 150 wird die Bestellsumme negativ, und die Kasse macht daraus eine Rückzahlung.Fix: Prüfung zurückholen oder in die Request-Validierung verschieben und percent=150 in die Tests aufnehmen.

Sicherheit

Die Stellen, an denen fremde Eingaben durchkommen: den Code auf Sicherheitslücken prüfen, eine SQL-Injection in der Suchfunktion erkennen, ein hartkodiertes Token im Skript finden und den Ausbruch aus dem Upload-Ordner stoppen.

SQL-Injection in der Suche

Die Adresse aus dem Formular wird in den SQL-Text geklebt — danach bestimmt derjenige über die Datenbank, der das Formular ausgefüllt hat.

Prompt

Prüfe diese Nutzersuche. Die E-Mail kommt direkt aus einem Formular auf der Website.def find_user(conn, email): query = "SELECT * FROM users WHERE email = '" + email + "'" return conn.execute(query).fetchone()

Ergebnis

Zeile 2 · kritisch · Sicherheitemail wird in den SQL-Text geklebt: Ein Wert mit einem Anführungszeichen beendet die Bedingung, und alles danach führt die Datenbank als deine eigene Abfrage aus — DROP TABLE eingeschlossen.Fix: ein Parameter statt Verkettung — conn.execute("SELECT id, email FROM users WHERE email = %s", [email]); damit fällt auch das SELECT * weg, das gerade den Passwort-Hash mitliest.

Token im Skript

Ein produktiver Schlüssel liegt im Repository und wandert nebenbei ins Build-Log.

Prompt

Prüfe dieses Deploy-Skript: Steckt hier etwas sicherheitstechnisch Gefährliches?#!/usr/bin/env bashAPI_TOKEN="sk_live_9f3c1ad84b22"echo "deploying with $API_TOKEN"curl -H "Authorization: Bearer $API_TOKEN" -X POST https://api.example.com/deploy

Ergebnis

Zeilen 2-3 · kritisch · SicherheitEin Produktionstoken steht in einer Datei im Git: Jeder, der das Repo je geklont hat, hat ihn in seiner History, und echo schreibt ihn zusätzlich ins Build-Log, das ein deutlich größerer Kreis liest.Fix: aus der Umgebung lesen ($API_TOKEN ohne Default), echo entfernen und den Schlüssel als geleakt behandeln — also rotieren.

Ausbruch aus dem Upload-Ordner

Der Dateiname kommt ungeprüft aus dem Request, und zwei Punkte mit Schrägstrich führen zu jeder Datei auf dem Server.

Prompt

Prüfe diesen Download-Endpunkt: Der Dateiname kommt aus dem Query-String.def download(request): name = request.args.get("file") path = os.path.join("/var/app/uploads", name) return send_file(path)

Ergebnis

Zeile 3 · kritisch · Sicherheitos.path.join verlässt /var/app/uploads anstandslos, wenn der Name ../../etc/passwd lautet, und ein absoluter Pfad verwirft das erste Argument sogar ganz: Jede für den Prozess lesbare Datei ist herunterladbar.Fix: os.path.basename(name), danach os.path.realpath des Ergebnisses mit dem Upload-Verzeichnis vergleichen und nur bei Treffer ausliefern.

Performance

Wo die Laufzeit verschwindet: den Code performanter machen, N+1-Abfragen in der Schleife finden, eine Abfrage entdecken, die am Index vorbeiläuft, und ein Logfile, das komplett im Speicher landet.

Abfragen in der Schleife

Der Report geht pro Nutzer zweimal in die Datenbank: Aus tausend Zeilen werden zweitausend Abfragen.

Prompt

Sieh dir diesen Report an: Bei tausend Nutzern braucht er eine Minute. Wohin geht die Zeit?def orders_report(user_ids): rows = [] for user_id in user_ids: user = db.query("SELECT name FROM users WHERE id = %s", user_id) orders = db.query("SELECT total FROM orders WHERE user_id = %s", user_id) rows.append((user.name, sum(o.total for o in orders))) return rows

Ergebnis

Zeilen 3-5 · schwer · PerformanceZwei Abfragen pro Nutzer: Bei tausend Nutzern sind das zweitausend Roundtrips, und die Zeit geht ins Netzwerk, nicht ins Rechnen.Fix: eine Abfrage mit JOIN und GROUP BY users.id, oder zwei Abfragen mit IN und das Zusammenführen im Speicher.

Abfrage am Index vorbei

Eine Funktion auf der Spalte und ein Prozentzeichen am Anfang von LIKE schalten die Indizes ab: Die Datenbank liest die ganze Tabelle.

Prompt

Prüfe diese Abfrage: Auf einer Tabelle mit zehn Millionen Zeilen läuft sie zwanzig Sekunden.SELECT *FROM ordersWHERE date_trunc('day', created_at) = '2026-09-01' AND lower(email) LIKE '%@example.com'ORDER BY created_at DESC

Ergebnis

Zeilen 3-4 · schwer · Performancedate_trunc auf created_at macht den Index nutzlos, und ein LIKE mit führendem Prozentzeichen kann ihn ohnehin nicht verwenden: Übrig bleibt ein Seq Scan über die gesamte Tabelle, und SELECT * holt zusätzlich Spalten, die niemand liest.Fix: created_at gegen einen Bereich vergleichen (ab Mitternacht des 1. September und kleiner als Mitternacht des 2.), die Domain separat indizieren oder als eigene Spalte führen und nur die benötigten Felder selektieren.

Das ganze Log im Speicher

Die Datei wird in einem Rutsch gelesen, damit wird die Größe des Logs zur Größe des Prozesses.

Prompt

Prüfe diesen Fehlerzähler für eine Logdatei. Die Dateien sind mehrere Gigabyte groß.def count_errors(path): lines = open(path).read().split("\n") return len([line for line in lines if "ERROR" in line])

Ergebnis

Zeile 2 · schwer · Performanceread() hebt die komplette Datei in den Speicher, split verdoppelt das, und die Liste in len() hält eine dritte Kopie: Bei acht Gigabyte Log ist der OOM-Killer vor dem Ergebnis da. Geschlossen wird die Datei außerdem nie.Fix: with open(path) as f und sum(1 for line in f if "ERROR" in line) — zeilenweise und mit konstantem Speicherbedarf.

Lesbarkeit und Standards

Vier verschachtelte ifs und Namen aus einem Buchstaben: den Code lesbarer machen, verschachtelte Bedingungen auflösen, Namen an den Styleguide angleichen und den Steuersatz aus zwei Stellen zusammenführen.

Vier verschachtelte Bedingungen

Die Regel für den Mailversand steckt auf der fünften Einrückungsebene und lässt sich nur am Stück lesen.

Prompt

Beurteile die Lesbarkeit dieser Funktion: Man muss sie bis zum Ende lesen, um die Bedingung zu kennen.def notify(user): if user is not None: if user.email is not None: if user.subscribed: if not user.banned: send_email(user.email) return True return False

Ergebnis

Zeilen 2-5 · gering · LesbarkeitVier verschachtelte ifs sind eine einzige Regel als Treppe: Wer wissen will, wer die Mail bekommt, muss alle vier Bedingungen gleichzeitig im Kopf halten — PEP 8 bittet um die flache Form.Fix: frühe Rückgaben — if user is None: return False und so weiter — dann bleibt der Rumpf auf einer Einrückungsebene.

Namen gegen den Styleguide

Ein Methodenname mit großem Anfangsbuchstaben und Variablen aus einem Buchstaben: Es meckert RuboCop, und der nächste Leser auch.

Prompt

Prüfe diese Methode gegen RuboCop: Was widerspricht hier dem üblichen Ruby-Stil?def CalcTotal(o) t = 0 o.each do |i| t = t + i.price * i.qty end tend

Ergebnis

Zeilen 1-4 · gering · StandardsNaming/MethodName: Methodennamen sind in Ruby snake_case, CamelCase liest sich hier wie eine Konstante. Die Namen o, t und i sagen nichts über ihren Inhalt, und das Aufsummieren von Hand ist genau die Aufgabe von sum.Fix: def calc_total(items) und items.sum do |item| item.price * item.qty end — aus vier Zeilen wird eine.

Steuersatz an zwei Stellen

Eine Regel in zwei Funktionen kopiert und schon auseinandergelaufen: 20 Prozent auf der Rechnung, 19 auf dem Beleg.

Prompt

Prüfe diese beiden Funktionen: Rechnen sie dasselbe?def invoice_total(order): return round(order.subtotal * 1.2, 2)def receipt_total(order): return round(order.subtotal * 1.19, 2)

Ergebnis

Zeilen 2 und 5 · schwer · ArchitekturEine Geschäftsregel steht zweimal im Code und ist bereits auseinandergelaufen: Die Rechnung nimmt 20 Prozent, der Beleg 19, und der Kunde sieht zwei verschiedene Summen für eine Bestellung.Fix: eine Konstante VAT_RATE und eine Funktion, die beide aufrufen — dann ändert sich der Satz an genau einer Stelle.

Tests und Verlässlichkeit

Was passiert, wenn etwas schiefgeht: die Testabdeckung über den guten Fall hinaus erweitern, eine verschluckte Exception im try-except finden und ein Backup entlarven, das nie auf Wiederherstellung geprüft wurde.

Test nur für den guten Fall

Ein einziger grüner Fall erzeugt den Eindruck einer Abdeckung, die es nicht gibt.

Prompt

Beurteile diese Tests für apply_discount: Was fehlt darin?def test_apply_discount(): order = Order(total=100) apply_discount(order, 10) assert order.total == 90

Ergebnis

Zeile 1 · schwer · TestsAbgedeckt ist ein Fall: ein normaler Rabatt auf eine unbezahlte Bestellung. Nichts zu null, zu hundert Prozent, zu negativen Werten, zu einer bereits bezahlten Bestellung oder zur Rundung bei 33,33 — jeder dieser Zweige kann unbemerkt brechen.Fix: parametrize für die Grenzwerte und ein eigener Test mit pytest.raises für percent=150 und für die bezahlte Bestellung.

Verschluckte Exception

except Exception: pass macht aus einem Absturz ein "ok" und löscht die Spur aus den Logs.

Prompt

Prüfe dieses Speichern des Profils: Nutzer berichten, dass Änderungen manchmal verschwinden.def save_profile(user, data): try: db.update(user.id, data) search.reindex(user.id) except Exception: pass return "ok"

Ergebnis

Zeilen 5-6 · kritisch · Bugexcept Exception: pass schluckt alles und gibt "ok" zurück, auch wenn nichts geschrieben wurde: Der Nutzer sieht Erfolg, die Daten fehlen, das Log ist leer. Zwei Operationen unter einem try machen einen Fehler der Reindizierung zusätzlich ununterscheidbar von einem Datenbankfehler.Fix: die konkreten Exceptions fangen, mit logger.exception protokollieren und einen ehrlichen Status zurückgeben; die Reindizierung herauslösen, damit ihr Fehler das Speichern nicht kippt.

Backup ohne Kontrolle

Das Skript ignoriert Exit-Codes und löscht die alten Kopien, bevor jemand merkt, dass die neue leer ist.

Prompt

Prüfe dieses nächtliche Backup-Skript: Eine Wiederherstellung aus der letzten Kopie hat nicht funktioniert.#!/usr/bin/env bashpg_dump "$DATABASE_URL" > /backup/db.sqlgzip -f /backup/db.sqlfind /backup -name "db.sql.gz" -mtime +7 -delete

Ergebnis

Zeilen 2-4 · kritisch · BugEs fehlt set -euo pipefail: Scheitert pg_dump, entsteht die Datei trotzdem — leer —, gzip komprimiert sie klaglos, und find löscht die funktionierenden Kopien älter als eine Woche; nach sieben Tagen ist kein einziges heiles Backup mehr da. Ein nicht gesetztes $DATABASE_URL fällt genauso wenig auf.Fix: set -euo pipefail in die erste Zeile, nach pg_dump die Größe des Dumps prüfen und alte Kopien erst nach einem erfolgreichen neuen Dump löschen.

Prompts fürs Code-Review: 16 fertige Prüfungen | iBro