Eccezioni in Python: try, except, else, finally, raise e le eccezioni personalizzate
Un'eccezione in Python è un oggetto che segnala un errore e interrompe il normale flusso del programma, finché non viene intercettata da un blocco try/except; else viene eseguito solo se non si è verificato alcun errore, finally sempre. Per sollevare un'eccezione si usa l'istruzione raise, per esempio raise ValueError("importo negativo"), mentre un'eccezione personalizzata si crea con una classe che eredita da Exception.
Un'eccezione in Python è un oggetto che segnala un errore e interrompe il normale svolgimento del programma. Se nessuno la intercetta, Python stampa il traceback e termina l'esecuzione. Le eccezioni si intercettano con il costrutto try/except e si sollevano con l'istruzione raise. Vediamo come funziona tutto questo, quali errori capitano più spesso e come evitare che la gestione degli errori diventi un modo per nasconderli.
Che cos'è un'eccezione in Python
Quando un'operazione non può essere eseguita — una divisione per zero, l'accesso a una chiave inesistente, la conversione di una parola in numero — Python crea un oggetto eccezione e lo «solleva». L'esecuzione della funzione corrente si ferma, l'eccezione risale la catena delle chiamate finché non trova un gestore adatto. Se non lo trova, il programma si interrompe.
>>> 10 / 0
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero
Una risposta alla domanda frequente: in Python la divisione per zero genera l'eccezione ZeroDivisionError. Riguarda sia /, sia //, sia %.
Come leggere il traceback
Il traceback è il rapporto su come il programma è arrivato all'errore. Conviene leggerlo dal basso verso l'alto. L'ultima riga è la più importante: il tipo di eccezione e il messaggio. Più in alto c'è la riga di codice in cui è successo tutto, ancora più su chi l'ha chiamata, e così via fino al punto di ingresso.
Traceback (most recent call last):
File "tracker.py", line 9, in <module>
total = parse_amount(row)
File "tracker.py", line 4, in parse_amount
return float(raw.strip())
~~~~~^^^^^^^^^^^^^
ValueError: could not convert string to float: 'cento'
Dalla versione 3.11 sotto la riga di codice compaiono i simboli ~~~^^^ (PEP 657): indicano quale espressione precisa della riga si è rotta. È particolarmente utile quando su una sola riga ci sono più chiamate una dopo l'altra.
Suggerimenti «Did you mean»
Dalla versione 3.10, in caso di errore di battitura nel nome, l'interprete propone una variante vicina. Nella Python 3.15 i suggerimenti sono diventati più intelligenti: l'interprete riconosce anche le abitudini provenienti da altri linguaggi. Una chiamata a list.push suggerisce append, mentre "caffè".toUpperCase() suggerisce il metodo upper. Il suggerimento tiene conto anche dei campi annidati: se un dizionario si trova nell'attributo totals di un oggetto e voi chiamate items() sull'oggetto stesso, l'errore propone .totals.items. Anche delattr() ora suggerisce nomi vicini.
Tipi di eccezioni in Python
Le eccezioni integrate sono decine, ma all'inizio incontrerete soprattutto queste:
| Eccezione | Quando si verifica | Esempio |
|---|---|---|
NameError | Il nome non è definito, spesso per un errore di battitura | print(totl) |
TypeError | L'operazione non è adatta a quel tipo | "5" + 3 |
ValueError | Il tipo è giusto, il valore no | int("cento") |
KeyError | La chiave non esiste nel dizionario | d["missing"] |
IndexError | Indice fuori dai limiti della lista | [1, 2][5] |
AttributeError | L'oggetto non ha quel metodo o quel campo | [].push(1) |
ZeroDivisionError | Divisione per zero | 1 / 0 |
FileNotFoundError | File non trovato | open("nope.txt") |
Un caso a parte è SyntaxError (e il suo caso particolare IndentationError): si verifica già durante l'analisi del file, prima dell'esecuzione del codice, quindi non può essere intercettato con try nello stesso file.
Gerarchia delle eccezioni
Le eccezioni sono classi e formano una gerarchia. In cima c'è BaseException. Da essa derivano Exception (quasi tutti gli errori «normali») e alcune eccezioni di sistema: KeyboardInterrupt, SystemExit, GeneratorExit.
BaseException
├── KeyboardInterrupt
├── SystemExit
└── Exception
├── ArithmeticError
│ └── ZeroDivisionError
├── LookupError
│ ├── KeyError
│ └── IndexError
├── OSError
│ └── FileNotFoundError
├── TypeError
└── ValueError
La gerarchia conta quando si intercetta: except LookupError cattura sia KeyError sia IndexError. Invece KeyboardInterrupt non è un errore nel codice: significa che l'utente ha premuto Ctrl+C. Non deriva da Exception apposta, perché except Exception non deve impedire di interrompere il programma.
Gestione delle eccezioni: try/except/else/finally
Il codice che potrebbe fallire va messo nel blocco try. Nel blocco except si indica il tipo di errore e che cosa fare. È proprio except il blocco che in Python si occupa della gestione delle eccezioni.
def parse_amount(raw):
try:
value = float(raw)
except ValueError as e:
print(f"Riga saltata: {e}")
return None
else:
return value # eseguito solo se non c'è stato errore
finally:
print("analisi completata") # eseguito sempre
print(parse_amount("250"))
L'ordine di stampa qui è: prima «analisi completata», poi 250.0. Il return nel blocco else memorizza il valore, poi Python esegue obbligatoriamente finally e solo dopo restituisce il risultato. Per questo finally è affidabile per la pulizia: chiudere un file, rilasciare una connessione.
Se nel finally si scrive un proprio return, esso sostituisce quello precedente e può perfino «inghiottire» l'eccezione. Dalla versione 3.14 per un codice del genere viene emesso un SyntaxWarning (PEP 765): non scrivete return dentro finally.
Più eccezioni insieme
Si possono scrivere più blocchi except per tipi diversi, oppure elencare i tipi in una tupla:
try:
amount = int(data["amount"])
except KeyError:
amount = 0
except (ValueError, TypeError) as e:
print("Importo non valido:", e)
amount = 0
I blocchi vengono controllati dall'alto verso il basso e si esegue il primo adatto, quindi i tipi più specifici vanno messi prima di quelli generici. Dalla versione 3.14 (PEP 758) più tipi si possono elencare anche senza parentesi — except ValueError, TypeError: — ma se c'è as, le parentesi restano obbligatorie.
Come stampare il testo dell'errore e conoscerne il tipo
L'oggetto eccezione è disponibile tramite as e. str(e) restituisce il messaggio, type(e).__name__ il nome del tipo:
try:
int("cento")
except ValueError as e:
print(type(e).__name__) # ValueError
print(e) # invalid literal for int() with base 10: 'cento'
Il modulo traceback: testo completo dell'errore
A volte l'errore va intercettato, ma bisogna conservare il traceback completo, per un log o un rapporto. Per questo esiste il modulo traceback:
import traceback
try:
1 / 0
except ZeroDivisionError:
traceback.print_exc() # stampa il traceback su stderr
text = traceback.format_exc() # lo stesso testo come stringa
Se scrivete i log con il modulo logging, è più semplice chiamare logging.exception("calcolo non riuscito") dentro except: il traceback viene aggiunto automaticamente al messaggio.
Python raise: come sollevare un'eccezione
L'istruzione raise solleva un'eccezione manualmente. Così una funzione comunica di non poter svolgere il lavoro con quei dati:
def add_expense(amount):
if amount < 0:
raise ValueError(f"L'importo non può essere negativo: {amount}")
...
add_expense(-5)
# ValueError: L'importo non può essere negativo: -5
Python raise è un'istruzione, non una funzione: dopo la parola raise non servono parentesi aggiuntive; tra le parentesi vanno gli argomenti del costruttore dell'eccezione. Scegliete il tipo in base al significato: valore errato — ValueError, tipo errato — TypeError.
raise senza argomenti e raise from
Dentro except, un raise senza argomenti rilancia l'eccezione corrente senza modifiche. Serve quando volete scrivere qualcosa nel log ma lasciare la gestione al codice chiamante.
Se sostituite un'eccezione con un'altra, usate raise ... from e: l'errore originale resta nella catena e nel traceback si vedono entrambi:
def load_config(path):
try:
with open(path) as f:
return f.read()
except FileNotFoundError as e:
raise RuntimeError(f"Configurazione mancante: {path}") from e
add_note: aggiungere contesto all'errore
Dalla versione 3.11 (PEP 678) a un'eccezione si può aggiungere una nota con il metodo add_note() e poi rilanciarla. Il tipo dell'errore non cambia, mentre la nota viene stampata sotto il messaggio:
def parse(raw, line_no):
try:
return float(raw)
except ValueError as e:
e.add_note(f"file expenses.csv, riga {line_no}")
raise
parse("cento", 17)
# ValueError: could not convert string to float: 'cento'
# file expenses.csv, riga 17
Come creare un'eccezione personalizzata in Python
Le eccezioni personalizzate servono per gli errori del dominio: budget superato, ordine già pagato, utente bloccato. Basta una classe che erediti da Exception:
class BudgetExceeded(Exception):
def __init__(self, category, limit, spent):
super().__init__(f"{category}: limite {limit}, speso {spent}")
self.category = category
self.limit = limit
self.spent = spent
def check_budget(category, limit, spent):
if spent > limit:
raise BudgetExceeded(category, limit, spent)
try:
check_budget("Cibo", 10000, 12500)
except BudgetExceeded as e:
print("Attenzione:", e) # Attenzione: Cibo: limite 10000, speso 12500
Ora il codice chiamante intercetta proprio il superamento del budget e non lo confonde con un errore di battitura. In un progetto grande conviene definire una classe base comune (class TrackerError(Exception)) e far derivare da essa tutte le proprie eccezioni: così si possono catturare tutte con un unico except TrackerError. Ereditate da Exception, non da BaseException.
Gruppi di eccezioni e except*
A volte gli errori sono più d'uno — per esempio durante la verifica di un intero file, oppure nelle attività parallele di asyncio.TaskGroup. Dalla versione 3.11 si possono raccogliere in un ExceptionGroup e gestire per tipo con except*:
def check_all(rows):
errors = []
for r in rows:
try:
float(r)
except ValueError as e:
errors.append(e)
if errors:
raise ExceptionGroup("errori nel file", errors)
try:
check_all(["1", "a", "b"])
except* ValueError as eg:
print(f"Righe errate: {len(eg.exceptions)}") # Righe errate: 2
Python try except per tutti gli errori: perché non si deve fare
L'abitudine più dannosa è intercettare tutto: except: pass. Il programma «smette di cadere», ma in realtà smette di segnalare i problemi. Un except senza tipo cattura anche KeyboardInterrupt e SystemExit, quindi il programma non si può più fermare con Ctrl+C. except Exception: pass è un po' meglio, ma inghiotte in silenzio anche i refusi nei nomi e gli errori di logica.
Come gestire correttamente le eccezioni in Python:
- intercettate solo il tipo che sapete gestire:
except ValueError, non tutto; - tenete il blocco
trycorto — solo le righe che possono fallire in quel modo; - se intercettate in modo ampio (
except Exception) al livello più alto del programma, registrate sempre il traceback; - non usate le eccezioni come normale ramificazione dove basta un controllo: in casi semplici meglio
dict.get()chetry/except KeyError; - per le risorse preferite
with: chiude il file anche in caso di errore.
Conclusioni
Le eccezioni in Python sono il meccanismo normale per segnalare gli errori, non un motivo per nasconderli. Leggete il traceback dal basso verso l'alto, intercettate tipi specifici con try/except, usate else per il codice «se è andato tutto bene» e finally per la pulizia. Python raise permette di segnalare un errore da sé, raise from e add_note conservano il contesto, mentre le eccezioni personalizzate ereditano da Exception. Nel corso di Python questo argomento è trattato in pratica: il tracker delle spese smette di cadere per una riga CSV sbagliata e segnala il superamento del budget con un'eccezione propria.
Domande frequenti
In che cosa un errore si differenzia da un'eccezione in Python?
Nel linguaggio comune sono sinonimi. Tecnicamente SyntaxError viene rilevato già durante l'analisi del codice, mentre gli altri errori si verificano in esecuzione sotto forma di oggetti eccezione che si possono intercettare.
Dopo un'eccezione si può proseguire dalla stessa riga?
No. Dopo l'intercettazione l'esecuzione riprende dopo il blocco try/except, non dalla riga che ha causato l'errore. Se volete elaborare ogni riga di un file, mettete il try dentro il ciclo.
Che cosa succede se un'eccezione si verifica dentro un except?
Viene sollevata la nuova eccezione, mentre quella originale viene conservata nell'attributo __context__. Nel traceback compare la frase During handling of the above exception, another exception occurred.
Come verificare nei test che una funzione sollevi un'eccezione?
In pytest si usa with pytest.raises(ValueError):, in unittest self.assertRaises(ValueError). Il test passa solo se nel blocco viene sollevata un'eccezione del tipo indicato.
Che cos'è assert e in che cosa differisce da raise?
assert condizione, "messaggio" solleva AssertionError se la condizione è falsa. È uno strumento di verifica in fase di sviluppo: con il flag -O le asserzioni vengono disattivate, quindi la validazione dell'input dell'utente va fatta con if e raise.
Questo argomento è trattato in modo approfondito, con pratica, nel corso «Python 3.15: от новичка до профи».
Vai al corso