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.
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
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.
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)
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.
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
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.
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
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.
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()
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.
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
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.
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)
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.
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
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.
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
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.
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])
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.
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
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.
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
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.
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)
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.
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
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.
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"
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.
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
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.