Prompt per spiegare il codice: dal regex allo stack trace
Capire che cosa fa un pezzo di codice altrui è metà del lavoro. Sedici domande già pronte per lo strumento: ognuna contiene un frammento, un pattern, un comando o una traccia di errore e dice a chi è rivolta la spiegazione e quanto deve scendere nel dettaglio.
Capire il codice di altri
Capire il codice scritto da altri quando nessuno lo può più spiegare: una funzione senza un commento letta riga per riga, un linguaggio che non scrivi e una scelta di cui resta solo il risultato.
Una funzione senza commenti
Trasforma il codice ereditato in una frase su quello che calcola davvero.
def recalc(items, rate): total = 0 for it in items: if it.status != "cancelled": total += it.price * it.qty if total > 10000: total = total * (1 - rate) return round(total, 2)# ereditata dallo sviluppatore precedente, nessun commento — spiega che cosa calcola questa funzione e a quali condizioni
La funzione calcola il totale dell’ordine con lo sconto.1. Moltiplica prezzo per quantità su tutte le righe e salta quelle annullate: una riga con status "cancelled" non entra mai nel totale.2. Se il totale supera 10000, gli viene applicato rate: un valore di 0.1 significa dieci per cento in meno.3. Il risultato viene arrotondato a due decimali, cioè ai centesimi.Il dettaglio che solo il codice racconta: lo sconto vale sull’intero totale e non sulla parte oltre la soglia. Un ordine da 10001, scontato, costa meno di uno da 9999.
Codice in un linguaggio che non conosci
Spiega un costrutto sconosciuto passando per quello che usi già nel tuo linguaggio.
import functoolsdef retry(times): def decorator(fn): @functools.wraps(fn) def wrapper(*args, **kwargs): for attempt in range(times): try: return fn(*args, **kwargs) except TimeoutError: continue raise RuntimeError("all attempts failed") return wrapper return decorator# io scrivo in PHP e vedo un decoratore per la prima volta — spiegamelo con concetti che già conosco
È un decoratore con parametro. La cosa più vicina che già conosci in PHP è una funzione che restituisce una closure avvolta attorno a un’altra funzione.Si legge dall’interno verso l’esterno:- retry(3) restituisce decorator;- decorator riceve la tua funzione e restituisce wrapper;- la riga @retry(3) sopra una funzione significa esattamente fn = retry(3)(fn) — da quel momento il nome punta a wrapper.wrapper chiama l’originale fino a tre volte e ingoia TimeoutError senza dire nulla, poi, finiti i tentativi, solleva RuntimeError. functools.wraps serve perché l’involucro conservi nome e docstring dell’originale: senza, il debugger e ogni traccia mostrerebbero soltanto wrapper.
Lettura riga per riga
Percorre il frammento una riga alla volta, per quando non è l’insieme a non tornare ma un punto preciso.
def top_customers(orders, n): totals = dict() for order in orders: totals[order.customer_id] = totals.get(order.customer_id, 0) + order.total ranked = sorted(totals.items(), key=lambda pair: -pair[1]) return ranked[:n]# spiegamelo riga per riga, soprattutto quella con sorted e il meno davanti a pair
1. totals = dict() — un dizionario vuoto dove si accumulano gli importi per cliente.2. Il ciclo scorre gli ordini. totals.get(id, 0) restituisce l’accumulato oppure zero se il cliente compare per la prima volta: per questo non serve un controllo a parte sul primo ordine.3. sorted su items() ordina coppie cliente-importo. Il meno davanti a pair[1] ribalta l’ordine: decrescente invece che crescente, così i clienti più grossi finiscono in cima.4. ranked[:n] tiene solo le prime n coppie.Restituisce le coppie e non i soli identificativi di proposito: a chi chiama serve anche l’importo per mostrarlo, e così non deve ricalcolarlo.
Perché è scritto proprio così
Spiega non che cosa fa il codice, ma perché l’autore ha scelto questa forma.
_cache = None_lock = threading.Lock()def get_config(): global _cache if _cache is None: with _lock: if _cache is None: _cache = load_config_from_disk() return _cache# perché il controllo su None è scritto due volte, prima del lock e dentro al lock
È il pattern double-checked locking, e i due controlli ci sono per motivi diversi.Il primo è la via veloce. Una volta caricata la configurazione, tutte le chiamate successive vedono il valore pronto e tornano subito senza toccare il lock. Prendere un lock costa tempo a ogni chiamata, mentre serve davvero una volta sola nella vita del processo.Il secondo riguarda la corsa. Mentre il primo thread tiene il lock e legge il file, un secondo ha già superato il controllo esterno e aspetta dietro. Senza il controllo interno ricaricherebbe la configurazione e sovrascriverebbe il risultato dell’altro.La riga global serve perché l’assegnamento modifichi la variabile del modulo invece di crearne una locale nuova.
Errori e stack trace
Il testo rosso va prima letto e solo dopo riparato: un'applicazione che va in crash, l'installazione di un pacchetto che non passa e una riga del log di produzione da decifrare.
L’applicazione va in crash
Legge lo stack trace: dove si è interrotto, quale riga è colpevole e che cosa significa l’errore in sé.
Traceback (most recent call last): File "app/orders.py", line 42, in create_order total = calc_total(items) File "app/pricing.py", line 17, in calc_total return sum(item.price * item.qty for item in items)AttributeError: 'dict' object has no attribute 'price'che cosa è successo qui e in quale riga devo cercare la causa
AttributeError significa che l’oggetto non ha un attributo chiamato price.Una traccia si legge dall’alto in basso per il percorso delle chiamate e dal basso in alto per la causa:- create_order, in orders.py riga 42, ha chiamato calc_total;- calc_total, in pricing.py riga 17, ha chiesto item.price e si è rotta.A rompersi è la riga 17, ma il problema non è lì. Il messaggio dice che dentro items ci sono dizionari e non oggetti, e su un dizionario l’accesso con il punto non funziona. Quindi chi chiama ha passato i dati così come arrivavano dal JSON, senza costruirci un modello. Si sistema dove items viene composto, oppure leggendo il campo come item["price"].
L’installazione non passa
Traduce il registro di installazione dalla lingua del gestore di pacchetti a quella di tutti.
ERROR: Could not find a version that satisfies the requirement pandas==2.9.1 (from versions: 2.0.0, 2.1.4, 2.2.2)ERROR: No matching distribution found for pandas==2.9.1non riesco a installare le dipendenze, che cosa significa questo messaggio
Quella versione della libreria non esiste.pip è andato nell’indice dei pacchetti, ha trovato pandas ed ha elencato le versioni disponibili: la più recente è la 2.2.2. Tu chiedi la 2.9.1, che nessuno pubblica, da qui il «no matching distribution».Nella pratica è quasi sempre una di tre cose: un refuso nel numero di versione, un requirements.txt arrivato da un progetto più recente, oppure una versione pubblicata solo per un’altra versione di Python. La seconda riga non è un problema a parte: è la conseguenza della prima.
Una riga del log di produzione
Spiega che cosa sta segnalando davvero il server quando nel log compaiono un timeout e tre numeri.
2026-09-12T03:14:07Z ERROR db pool: timeout acquiring connection after 30s, active=20 idle=0 waiting=1372026-09-12T03:14:07Z ERROR api POST /orders 503 in 30012ms request_id=8c1f2ache cosa sta cercando di dirmi il server con queste due righe
Sono finite le connessioni al database.Prima riga: il pool ha aspettato una connessione libera per 30 secondi e ha rinunciato. active=20 vuol dire che tutte e venti sono occupate, idle=0 che non ne resta nessuna libera, waiting=137 che dietro ci sono centotrentasette richieste in coda.La seconda riga è la conseguenza: POST /orders è rimasta appesa per gli stessi 30 secondi e ha risposto 503.Lette insieme: le richieste arrivano più in fretta di quanto il database liberi connessioni. Il pool non è rotto, sta dicendo onestamente di aver toccato il soffitto. Il passo successivo è guardare le query lunghe che tengono impegnata una connessione e la dimensione del pool rispetto al numero di worker.
Espressioni regolari e comandi
Righe che funzionano finché nessuno le tocca: capire che cosa controlla un'espressione regolare di validazione, una presa da un file di configurazione e una catena di comandi copiata da una procedura.
Che cosa controlla il regex
Smonta il pattern nei suoi pezzi e dice quali indirizzi passano e quali vengono respinti a torto.
^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z][A-Za-z]+$sta nella validazione del nostro modulo — che cosa controlla esattamente e che cosa lascia passare di troppo
Il pattern controlla la forma di un indirizzo, non la sua esistenza.- ^ e $ pretendono che corrisponda l’intera stringa e non un pezzo al suo interno.- La parte prima della chiocciola: lettere, cifre, punto, trattino basso, percento, più e trattino, almeno un carattere.- @ — esattamente una chiocciola.- Il dominio: lettere, cifre, punti e trattini.- Alla fine un punto e un’estensione di almeno due lettere.Che cosa passa di troppo: due punti di fila, un punto a inizio nome, un dominio che non esiste. Che cosa viene respinto senza motivo: gli indirizzi internazionalizzati e le estensioni in alfabeti non latini. Per un modulo di solito basta, perché l’esistenza la conferma comunque la mail di verifica.
Un’espressione presa dalla configurazione
Mostra quali parti della riga di log finiscono in quale gruppo e in che ordine.
^([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+) - - \[([^\]]+)\] "([A-Z]+) ([^ ]+) [^"]+" ([0-9]+) ([0-9]+)$serve a scomporre una riga di access.log — che cosa finisce in ciascun gruppo
L’espressione scompone una riga del log del server web in sei gruppi.1. L’indirizzo IP: quattro numeri separati da punti. Il punto con la barra davanti indica un punto vero e non «un carattere qualsiasi».2. La data fra parentesi quadre: dentro è ammesso qualsiasi carattere tranne la quadra di chiusura, ed è proprio questo a impedire al gruppo di divorare il resto della riga.3. Il metodo: lettere maiuscole.4. Il percorso: tutto fino al primo spazio.5. Il codice di risposta.6. La dimensione della risposta in byte.L’ordine dei gruppi è l’ordine dei campi nel risultato: il primo è l’indirizzo, il quinto lo stato. Una riga il cui percorso contenga uno spazio non corrisponde per intero e viene semplicemente saltata.
Un comando copiato da una procedura
Smonta la catena del terminale prima che tu prema Invio su un server di produzione.
tar -czf - ./data | ssh backup@nas "cat > /backups/data-$(date +%F).tar.gz"# trovato nella nostra procedura di backup — spiegami che cosa succederà prima che lo esegua
Il comando copia la cartella data su un’altra macchina senza salvare nulla su questa.- tar -czf - costruisce l’archivio e lo comprime, ma invece che in un file lo scrive sullo standard output: è questo il senso del trattino isolato.- La barra verticale consegna quel flusso al comando successivo.- ssh si collega alla macchina nas con l’utente backup e lì avvia cat, che deposita il flusso in un file.- $(date +%F) inserisce la data di oggi nel nome: viene fuori data-2026-09-13.tar.gz.L’archivio non atterra da nessuna parte per intero: viaggia sulla rete mentre viene creato, quindi sul disco di partenza non serve spazio libero. C’è un solo spigolo: se un file con quel nome esiste già, viene sovrascritto senza una parola.
SQL e query sul database
Capire una query di report lunga mezzo schermo, farsi spiegare una funzione finestra con parole normali e scoprire dove la stessa istruzione inciampa in produzione ma non sul database di prova.
Una query di report
Traduce un SELECT lungo in una frase su quali righe finiranno nel report.
SELECT c.name, date_trunc('month', o.created_at) AS month, count(*) AS orders, sum(o.total) AS revenueFROM orders oJOIN customers c ON c.id = o.customer_idWHERE o.created_at >= now() - interval '1 year'GROUP BY 1, 2HAVING sum(o.total) > 1000ORDER BY revenue DESC-- spiega che cosa restituisce questa query e perché una parte dei clienti non compare mai nel report
La query calcola il fatturato per cliente, suddiviso per mese, sull’ultimo anno.- JOIN collega l’ordine al cliente. È una join normale, non LEFT, quindi i clienti senza nemmeno un ordine non compaiono affatto.- WHERE taglia via tutto ciò che è più vecchio di un anno.- date_trunc riporta la data al primo del mese: è così che i singoli ordini si raccolgono in gruppi mensili.- GROUP BY 1, 2 raggruppa sulle prime due espressioni dell’elenco: nome del cliente e mese.- HAVING scarta i gruppi sotto 1000. A differenza di WHERE questo filtro interviene quando la somma è già stata calcolata.Una riga del risultato è un cliente in un mese, non un cliente nel complesso.
Funzione finestra
Spiega che cosa calcola OVER e in che cosa una finestra differisce da un normale raggruppamento.
SELECT customer_id, created_at, total, sum(total) OVER (PARTITION BY customer_id ORDER BY created_at) AS running_total, row_number() OVER (PARTITION BY customer_id ORDER BY total DESC) AS rnFROM orders-- spiega che cosa calcolano questi due OVER e perché le righe non vengono compattate
Una funzione finestra calcola su un gruppo senza compattare le righe: in uscita ce ne sono quante ne sono entrate, solo che ognuna guadagna due colonne nuove.- PARTITION BY customer_id divide la tabella in mucchi per cliente. Il mucchio è la finestra.- Nella prima colonna la somma è progressiva: ORDER BY created_at fissa l’ordine dentro la finestra e, per impostazione predefinita, si contano le righe dall’inizio della finestra fino a quella corrente. Al terzo ordine di un cliente è la somma dei primi tre.- Il secondo OVER ordina in modo diverso, per importo decrescente, e row_number assegna le posizioni: l’1 va all’ordine più grosso di quel cliente.È esattamente questo a distinguere una finestra da GROUP BY: il raggruppamento avrebbe lasciato una riga per cliente e perso i singoli ordini.
Dove la query inciampa
Spiega la query e segnala anche contro che cosa andrà a sbattere con i dati veri.
SELECT *FROM orders oLEFT JOIN order_items i ON i.order_id = o.idWHERE DATE(o.created_at) = '2026-09-01' AND o.status != 'cancelled'ORDER BY o.created_at DESC-- sul database di prova è istantanea, in produzione resta appesa — spiegami la query e contro che cosa va a sbattere qui
La query prende gli ordini del primo settembre insieme a tutte le loro righe. LEFT JOIN vuol dire che esce anche un ordine senza righe, con le colonne di order_items vuote.Dove inciampa con i dati veri:- DATE(o.created_at) è una funzione messa sopra la colonna, e per questo l’indice su created_at diventa inutilizzabile: il database deve calcolare una data per ogni riga della tabella. Su cento righe di prova non si vede, su milioni è una scansione completa.- SELECT * su una relazione uno-a-molti restituisce ogni ordine tante volte quante sono le sue righe. Escono molte più righe che ordini, e l’applicazione deve ricompattarle da sola.- Le due tabelle hanno colonne con lo stesso nome (id, created_at): nel risultato si mescolano e dal nome non si capisce più da quale tabella arrivi un campo.
Spiegare a un principiante e al colloquio
Quando la spiegazione conta più del codice: il primo programma di chi impara a programmare da zero, un esercizio da colloquio di lavoro e uno script da spiegare a un responsabile che non programma.
Il primo programma
Una spiegazione per chi scrive codice da due settimane: che cosa fa ogni riga e perché sta lì.
nums = [4, 8, 15, 16, 23, 42]total = 0for n in nums: if n % 2 == 0: total += nprint(total / len(nums))# ho appena iniziato con Python — spiegami passo passo che cosa succede qui
Il programma somma i numeri pari della lista e divide il risultato per la quantità di TUTTI i numeri.1. nums è una lista di sei numeri.2. total = 0 è la scatola in cui accumuleremo. Lo zero serve per avere un punto di partenza.3. for n in nums è il ciclo: n diventa a turno ciascun numero della lista.4. n % 2 == 0 vuol dire che il resto della divisione per due è zero, cioè che il numero è pari.5. total += n aggiunge quanto trovato all’accumulato.6. La divisione per len(nums) è la divisione per la lunghezza della lista, cioè per sei.Sembra un errore di impostazione: nella lista ci sono quattro numeri pari che sommano 70, ma si divide per sei. Se vuoi la media dei pari, devi dividere per quanti sono i pari.
Un esercizio da colloquio
L’algoritmo smontato in modo da poterlo raccontare ad alta voce davanti a una lavagna.
def search(nums, target): lo, hi = 0, len(nums) - 1 while hi >= lo: mid = (lo + hi) // 2 if nums[mid] == target: return mid if nums[mid] > target: hi = mid - 1 else: lo = mid + 1 return -1# me l’hanno chiesto al colloquio — spiegami l’idea dell’algoritmo e perché è veloce
È una ricerca binaria su un array ordinato. L’idea è una sola: a ogni passo si butta via metà di quello che resta.- lo e hi sono i confini del tratto in cui la risposta può ancora trovarsi.- mid è il centro. La doppia barra è la divisione intera, per questo esce un indice e non una frazione.- Se al centro c’è il valore cercato, è finita.- Se il centro è più grande, la risposta sta a sinistra e si sposta hi.- Altrimenti sta a destra e si sposta lo.- Il ciclo finisce quando i confini si incrociano: il valore non c’è e torna -1.Il costo è logaritmico: un milione di elementi significa una ventina di passi invece di un milione. La condizione indispensabile è che l’array sia già ordinato, altrimenti l’algoritmo restituisce in silenzio una risposta sbagliata.La domanda di rincalzo tipica al colloquio: nei linguaggi con interi di dimensione fissa lo + hi può andare in overflow, per questo lì si scrive lo + (hi - lo) // 2.
Spiegarlo al responsabile
Il racconto dello script senza un solo termine tecnico, per parlare con chi il codice non lo legge.
import csv, smtplibrows = list(csv.DictReader(open("orders.csv")))late = [r for r in rows if r["shipped_at"] == ""]body = "Ordini non ancora spediti: " + str(len(late))smtplib.SMTP("mail.local").sendmail("bot@shop.local", "ops@shop.local", body)# devo spiegare a un responsabile non tecnico che cosa fa questo script
Lo script controlla quali ordini non sono ancora partiti e ne comunica il numero via mail.Passo per passo, senza termini tecnici:1. Apre la tabella degli ordini: un file normale, che aprirebbe anche Excel.2. Tiene le righe in cui la data di spedizione è vuota, cioè ordini fatti ma non partiti.3. Conta quante righe di questo tipo ci sono.4. Manda una mail all’indirizzo del reparto con quel solo numero nel testo.Quello che un responsabile deve sapere: lo script non cambia nulla e non disturba nessuno, si limita a leggere e a segnalare, quindi può girare anche ogni ora. Il suo punto debole è uno: legge un file, e quindi mostra la situazione all’ultima esportazione e non quella di adesso.