Prompt per la revisione del codice: 16 casi pronti
Una revisione del codice fatta bene trova il difetto prima della produzione. Sedici frammenti con un errore vero ciascuno e la domanda che ci farebbe sopra un revisore: sostituisci l'esempio con il tuo codice e premi Applica.
Bug e logica
Codice che gira e mente lo stesso: gli errori logici che il compilatore non segnala, uno zero scambiato per assenza, un orario di un altro fuso e una regressione arrivata con una pull request.
Argomento mutabile di default
La trappola classica di Python: la lista nella firma sopravvive alla chiamata e raccoglie dati altrui.
Rivedi questa funzione del carrello: i clienti trovano nell’ordine articoli che non hanno mai aggiunto.def add_item(item, cart=[]): cart.append(item) return cart
Riga 1 · critico · bugcart=[] viene creato una volta sola, all’import del modulo, e non a ogni chiamata: il secondo cliente continua a riempire il carrello del primo, e così via finché il processo non riparte.Correzione: cart: list | None = None e, come prima riga del corpo, if cart is None: cart = [].
Uno zero scambiato per assenza
Il controllo di verità nasconde uno zero legittimo e il cliente a saldo vuoto vede un segnaposto.
Guarda questo controllo: distingue davvero un valore assente da uno zero?def render_balance(user): balance = user.get("balance") if not balance: return "nessun dato" return format_money(balance)
Riga 3 · grave · bugif not balance prende allo stesso modo None, 0 e 0.0: chi ha speso fino all’ultimo centesimo legge "nessun dato" invece di uno zero onesto.Correzione: if balance is None — l’assenza si controlla in modo esplicito e lo zero passa al formattatore come qualsiasi altro numero.
Confronto di orari senza fuso
datetime.now() restituisce l’ora locale del server mentre la scadenza del token è salvata in UTC.
Controlla il calcolo della scadenza del token. Il servizio gira su server in fusi diversi.def is_expired(token): return datetime.now() > token.expires_at
Riga 2 · critico · bugdatetime.now() è ora locale naive, expires_at arriva dal database in UTC: su un server italiano i token vivono due ore di troppo e, contro un valore aware, il confronto salta subito con TypeError.Correzione: datetime.now(timezone.utc) e una regola per tutto il progetto — si salva solo aware e in UTC.
Regressione in una pull request
Una revisione della modifica e non del file: che cosa va in produzione con questo commit.
Rivedi questo diff di pull request: che cosa va esattamente in produzione insieme a lui?@@ -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
Righe 15-16 · critico · bugIl controllo rimosso era l’unico limite su percent: con 150 il totale dell’ordine diventa negativo e la cassa lo trasforma in un rimborso.Correzione: rimettere il controllo o spostarlo nella validazione della richiesta, e aggiungere il caso percent=150 ai test.
Sicurezza
I punti in cui l'input altrui arriva al database: come si previene una SQL injection nella ricerca, che cosa succede a un token scritto dentro lo script e come si esce dalla cartella di upload.
SQL injection nella ricerca
L’indirizzo del modulo viene concatenato dentro l’SQL: a comandare il database è chi ha compilato il modulo.
Rivedi questa ricerca utente. L’email arriva direttamente da un modulo del sito.def find_user(conn, email): query = "SELECT * FROM users WHERE email = '" + email + "'" return conn.execute(query).fetchone()
Riga 2 · critico · sicurezzaemail viene incollata nel testo SQL: un valore con un apice chiude la condizione e tutto ciò che segue il database lo esegue come una tua query, DROP TABLE compreso.Correzione: un parametro al posto della concatenazione — conn.execute("SELECT id, email FROM users WHERE email = %s", [email]); così sparisce anche il SELECT * che oggi si porta dietro l’hash della password.
Token dentro lo script
Una chiave di produzione sta nel repository e per giunta finisce stampata nel log di deploy.
Rivedi questo script di deploy: c’è qualcosa di pericoloso dal punto di vista della sicurezza?#!/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
Righe 2-3 · critico · sicurezzaUn token di produzione è scritto in un file versionato: ce l’ha nella cronologia chiunque abbia clonato il repository, ed echo lo ricopia nel log di deploy, che legge molta più gente.Correzione: leggerlo dall’ambiente ($API_TOKEN senza valore di default), togliere l’echo e considerare la chiave compromessa — quindi ruotarla.
Uscita dalla cartella di upload
Il nome del file arriva dalla richiesta così com’è, e due punti con la barra portano a qualsiasi file del server.
Rivedi questo endpoint di download: il nome del file arriva dalla query.def download(request): name = request.args.get("file") path = os.path.join("/var/app/uploads", name) return send_file(path)
Riga 3 · critico · sicurezzaos.path.join esce tranquillamente da /var/app/uploads quando il nome è ../../etc/passwd, e un percorso assoluto scarta del tutto il primo argomento: si scarica qualsiasi file leggibile dal processo.Correzione: os.path.basename(name), poi confrontare os.path.realpath del risultato con la cartella di upload e servire il file solo se sta dentro.
Prestazioni
Ottimizzare le prestazioni dove si perde davvero tempo: una query dentro un ciclo che diventa un problema N+1, una ricerca che gira senza indice e un log enorme letto tutto in memoria.
Query dentro il ciclo
Il report va nel database due volte per ogni utente: mille righe diventano duemila query.
Guarda questo report: con mille utenti impiega un minuto. Dove se ne va il tempo?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
Righe 3-5 · grave · prestazioniDue query per utente: mille utenti sono duemila andate e ritorni, e il tempo se ne va nella rete, non nei calcoli.Correzione: una query con JOIN e GROUP BY users.id, oppure due query con IN e l’unione fatta in memoria.
Query che manca l’indice
Una funzione sulla colonna e una percentuale a inizio LIKE spengono gli indici: il database legge tutta la tabella.
Rivedi questa query: su una tabella da dieci milioni di righe impiega venti secondi.SELECT *FROM ordersWHERE date_trunc('day', created_at) = '2026-09-01' AND lower(email) LIKE '%@example.com'ORDER BY created_at DESC
Righe 3-4 · grave · prestazionidate_trunc su created_at rende l’indice inutile e una LIKE che comincia con la percentuale non può usarlo per definizione: resta un seq scan sull’intera tabella, e SELECT * si trascina dietro colonne che nessuno legge.Correzione: confrontare created_at con un intervallo (dalla mezzanotte del 1 settembre e minore della mezzanotte del 2), indicizzare il dominio a parte o tenerlo in una colonna propria, e selezionare solo i campi che servono.
Tutto il log in memoria
Il file viene letto in un colpo solo, così la dimensione del log diventa la dimensione del processo.
Rivedi questo contatore di errori su un log. I file arrivano a diversi gigabyte.def count_errors(path): lines = open(path).read().split("\n") return len([line for line in lines if "ERROR" in line])
Riga 2 · grave · prestazioniread() porta in memoria l’intero file, split raddoppia il conto e la lista dentro len() ne tiene una terza copia: su un log da otto gigabyte l’OOM killer arriva prima del risultato. Il file, tra l’altro, non viene mai chiuso.Correzione: with open(path) as f e sum(1 for line in f if "ERROR" in line) — riga per riga e con memoria costante.
Leggibilità e standard
Rendere il codice leggibile per chi verrà dopo: quattro condizioni annidate da sciogliere con un early return, nomi di una lettera contro la guida di stile e l'IVA copiata in due funzioni.
Quattro condizioni annidate
La regola per inviare l’email si nasconde al quinto livello di indentazione e si legge solo tutta insieme.
Valuta la leggibilità di questa funzione: bisogna leggerla fino in fondo per conoscere la condizione.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
Righe 2-5 · minore · leggibilitàQuattro if annidati sono un’unica regola distesa a scala: per sapere a chi arriva l’email bisogna tenere in testa tutte e quattro le condizioni insieme, mentre PEP 8 chiede la forma piatta.Correzione: uscite anticipate — if user is None: return False e così via — e il corpo resta su un solo livello di indentazione.
Nomi contro la guida di stile
Un metodo con l’iniziale maiuscola e variabili di una lettera: protesta RuboCop e protesta chi legge dopo.
Rivedi questo metodo con RuboCop: che cosa qui contraddice il normale stile Ruby?def CalcTotal(o) t = 0 o.each do |i| t = t + i.price * i.qty end tend
Righe 1-4 · minore · standardNaming/MethodName: in Ruby i nomi dei metodi sono snake_case e il CamelCase qui si legge come una costante. I nomi o, t e i non dicono nulla del contenuto, e accumulare a mano è esattamente il lavoro di sum.Correzione: def calc_total(items) e items.sum do |item| item.price * item.qty end — quattro righe stanno in una.
IVA in due posti
La stessa regola copiata in due funzioni e già divergente: 20 per cento in fattura, 19 sullo scontrino.
Rivedi queste due funzioni: calcolano la stessa cosa?def invoice_total(order): return round(order.subtotal * 1.2, 2)def receipt_total(order): return round(order.subtotal * 1.19, 2)
Righe 2 e 5 · grave · architetturaUna regola di business scritta due volte è già divergente: la fattura applica il 20 per cento, lo scontrino il 19, e il cliente vede due totali diversi per un solo ordine.Correzione: una costante VAT_RATE e una sola funzione usata da entrambe, così l’aliquota si cambia in un punto solo.
Test e affidabilità
Che cosa succede quando qualcosa si rompe: test che coprono solo il percorso felice, un'eccezione catturata e ignorata senza nemmeno un log e un backup che nessuno ha mai provato a ripristinare.
Test solo del percorso felice
Un unico caso verde dà l’impressione di una copertura che non c’è.
Valuta questi test di apply_discount: che cosa manca?def test_apply_discount(): order = Order(total=100) apply_discount(order, 10) assert order.total == 90
Riga 1 · grave · testÈ coperto un caso solo: uno sconto normale su un ordine non pagato. Niente sullo zero, sul cento per cento, su un valore negativo, su un ordine già pagato o sull’arrotondamento di 33,33 — ognuno di quei rami può rompersi in silenzio.Correzione: parametrize sui valori limite e un test a parte con pytest.raises per percent=150 e per l’ordine già pagato.
Eccezione ingoiata
except Exception: pass trasforma un errore in "ok" e cancella la traccia dai log.
Rivedi questo salvataggio del profilo: gli utenti dicono che a volte le modifiche spariscono.def save_profile(user, data): try: db.update(user.id, data) search.reindex(user.id) except Exception: pass return "ok"
Righe 5-6 · critico · bugexcept Exception: pass ingoia tutto e restituisce "ok" anche quando la scrittura non è avvenuta: l’utente vede il successo, i dati non ci sono e il log è vuoto. Con due operazioni sotto lo stesso try, un errore della reindicizzazione non si distingue da un errore del database.Correzione: catturare le eccezioni specifiche, registrare con logger.exception e restituire uno stato onesto; spostare fuori la reindicizzazione, perché il suo errore non annulli il salvataggio.
Backup senza verifica
Lo script ignora i codici di uscita e cancella le copie vecchie prima che qualcuno scopra che la nuova è vuota.
Rivedi questo script di backup notturno: il ripristino dall’ultima copia non è riuscito.#!/usr/bin/env bashpg_dump "$DATABASE_URL" > /backup/db.sqlgzip -f /backup/db.sqlfind /backup -name "db.sql.gz" -mtime +7 -delete
Righe 2-4 · critico · bugManca set -euo pipefail: se pg_dump fallisce il file viene creato lo stesso, vuoto, gzip lo comprime senza battere ciglio e find cancella le copie buone più vecchie di una settimana; dopo sette giorni non ne resta una integra. Un $DATABASE_URL non impostato passa altrettanto inosservato.Correzione: set -euo pipefail in prima riga, controllo della dimensione del dump dopo pg_dump e cancellazione delle copie vecchie solo dopo che la nuova è riuscita.