{ } Programmazione

Leggere uno stack trace Python: guida riga per riga

Come leggere un traceback Python: ordine delle chiamate, tipo di eccezione, eccezioni concatenate ed errori più comuni, con esempi riga per riga.

Per chi inizia a programmare, uno stack trace è un muro di testo rosso che annuncia un fallimento. Per chi ha esperienza, è una delle informazioni più utili che un programma possa dare: una mappa precisa del punto in cui le cose sono andate storte e del percorso seguito per arrivarci. Imparare a leggerlo con metodo fa risparmiare una quantità enorme di tempo.

Cos'è uno stack trace

Quando un programma chiama una funzione, che a sua volta ne chiama un'altra, l'interprete tiene traccia di questa catena in una struttura chiamata call stack, la pila delle chiamate. Se si verifica un'eccezione che nessuno gestisce, Python stampa il contenuto della pila in quel momento: è il traceback, il nome che Python dà allo stack trace.

In pratica, il traceback risponde a tre domande:

  1. che cosa è successo (tipo di eccezione e messaggio);
  2. dove è successo (file, riga, funzione);
  3. come ci si è arrivati (la sequenza di chiamate).

Un esempio commentato

Consideriamo questo programma:

def carica_ordini(righe):
    return [analizza_riga(r) for r in righe]

def analizza_riga(riga):
    codice, quantita = riga.split(";")
    return codice, int(quantita)

carica_ordini(["A12;3", "B07;due"])

L'esecuzione produce:

Traceback (most recent call last):
  File "ordini.py", line 8, in <module>
    carica_ordini(["A12;3", "B07;due"])
  File "ordini.py", line 2, in carica_ordini
    return [analizza_riga(r) for r in righe]
  File "ordini.py", line 6, in analizza_riga
    return codice, int(quantita)
ValueError: invalid literal for int() with base 10: 'due'

La prima riga, «most recent call last», è la chiave di lettura: le chiamate sono elencate dalla più vecchia alla più recente. In Python, quindi, la parte più importante è in fondo.

Come leggerlo, nell'ordine giusto

  1. Parti dall'ultima riga. ValueError: invalid literal for int() with base 10: 'due' dice che si è tentato di convertire in intero la stringa 'due'.
  2. Risali al frame più recente. Riga 6 di analizza_riga: è lì che l'errore si è manifestato.
  3. Continua a risalire per capire il contesto: analizza_riga è stata chiamata da carica_ordini, che a sua volta è stata chiamata dal codice principale alla riga 8.
  4. Chiediti dove nasce davvero il problema. Non sempre coincide con la riga dell'errore. Qui il codice è corretto: è il dato "B07;due" a non rispettare il formato atteso. La correzione giusta potrebbe essere validare l'input, non modificare la conversione.

Il tuo codice e quello delle librerie

Nei progetti reali i traceback includono spesso decine di frame di librerie esterne o della libreria standard. Un trucco pratico è cercare i frame che appartengono ai tuoi file. L'errore può manifestarsi in profondità dentro una libreria, ma quasi sempre la causa è un argomento sbagliato passato dal tuo codice. Il frame più in basso tra quelli del tuo progetto è di solito il punto da cui partire.

Eccezioni concatenate

A volte il traceback contiene due blocchi separati da una frase. Le due varianti hanno significati diversi:

Frase nel traceback Significato
«During handling of the above exception, another exception occurred» Mentre si gestiva un'eccezione, se n'è verificata un'altra, spesso nel blocco except stesso
«The above exception was the direct cause of the following exception» Un'eccezione è stata rilanciata in modo esplicito con raise ... from ...

In entrambi i casi conviene leggere tutti i blocchi: il primo contiene spesso la causa originale, il secondo il modo in cui il programma ha reagito.

Le eccezioni più comuni

Riconoscere al volo i tipi più frequenti accelera molto la diagnosi:

  • NameError: un nome non definito, spesso un refuso o una variabile usata prima di essere assegnata;
  • TypeError: un'operazione su un tipo inadatto, come sommare una stringa e un numero, o una funzione chiamata con argomenti sbagliati;
  • AttributeError: un attributo inesistente; il caso classico è 'NoneType' object has no attribute ..., che indica una funzione che ha restituito None quando ci si aspettava un oggetto;
  • KeyError: una chiave assente in un dizionario;
  • IndexError: un indice oltre la fine di una lista;
  • ValueError: un tipo corretto ma con un valore non accettabile, come nell'esempio sopra.

Le versioni recenti di Python hanno migliorato molto i messaggi, aggiungendo suggerimenti come «Did you mean...?» e indicatori che sottolineano la parte esatta dell'espressione che ha causato l'errore. Vale la pena leggerli con attenzione.

Traceback nei log

In produzione nessuno vede il terminale: il traceback deve finire nei log. Il modulo logging lo registra automaticamente se, all'interno di un blocco except, usi logger.exception("messaggio"). Un messaggio di log senza traceback, del tipo «Errore durante l'elaborazione», costringe a indovinare; uno con il traceback completo indica subito il punto da esaminare.

In sintesi

  • In Python lo stack trace si legge dal basso: l'ultima riga indica tipo e messaggio dell'eccezione.
  • Risalendo i frame ricostruisci il percorso che ha portato all'errore.
  • La riga in cui l'errore si manifesta non è sempre quella in cui nasce il problema.
  • Cerca i frame del tuo codice tra quelli delle librerie e leggi tutti i blocchi delle eccezioni concatenate.
  • In produzione registra sempre il traceback completo nei log con logger.exception.
MM
Marco Marzaduri

Marco Marzaduri si occupa di cybersecurity, informatica forense e sviluppo web. Laureato in informatica e in psicologia, unisce l'analisi tecnica alla comprensione del fattore umano.

Leggi anche