Introduzione
Quando un programma esegue un’unica operazione alla volta, la gestione delle eccezioni è relativamente semplice: si verifica un errore, viene sollevata un’eccezione e il codice la intercetta. La situazione cambia nei programmi concorrenti, ad esempio quando vengono eseguite più attività asincrone contemporaneamente.
In questi casi, diverse attività possono terminare con errori nello stesso momento. Gestire soltanto la prima eccezione significherebbe perdere informazioni importanti sulle altre operazioni fallite. A partire da Python 3.11, il linguaggio offre ExceptionGroup e la sintassi except*, progettate proprio per rappresentare e gestire gruppi di eccezioni.
Questo meccanismo è particolarmente utile quando si eseguono più controlli indipendenti, chiamate a servizi esterni o attività asincrone. Invece di interrompere immediatamente il flusso al primo errore, è possibile raccogliere tutti i problemi e analizzarli in modo ordinato.
Codice completo
import asyncio
async def verifica_servizio(nome: str, ritardo: float, funzionante: bool) -> str:
"""Simula il controllo di un servizio remoto."""
await asyncio.sleep(ritardo)
if not funzionante:
raise ConnectionError(f"Il servizio ´{nome}´ non è raggiungibile")
return f"Il servizio ´{nome}´ è operativo"
async def esegui_verifiche() -> None:
"""
Esegue più verifiche concorrenti.
TaskGroup raccoglie automaticamente le eccezioni
sollevate dalle attività figlie.
"""
async with asyncio.TaskGroup() as gruppo:
gruppo.create_task(verifica_servizio("pagamenti", 0.2, False))
gruppo.create_task(verifica_servizio("autenticazione", 0.1, True))
gruppo.create_task(verifica_servizio("notifiche", 0.3, False))
def avvia_controlli() -> None:
try:
asyncio.run(esegui_verifiche())
except* ConnectionError as gruppo_errori:
print("Sono stati rilevati problemi di connessione:")
for errore in gruppo_errori.exceptions:
print(f"- {errore}")
except* Exception as gruppo_errori:
print("Si sono verificati errori non previsti:")
for errore in gruppo_errori.exceptions:
print(f"- {type(errore).__name__}: {errore}")
if __name__ == "__main__":
avvia_controlli()
Salvando il codice in un file, ad esempio controlli.py, è possibile eseguirlo con Python 3.11 o una versione successiva. L’output sarà simile al seguente:
Sono stati rilevati problemi di connessione:
- Il servizio ´pagamenti´ non è raggiungibile
- Il servizio ´notifiche´ non è raggiungibile
Spiegazione
Che cos’è ExceptionGroup
ExceptionGroup è un contenitore di eccezioni. Invece di memorizzare un solo errore, può contenere più oggetti eccezione, anche di tipi differenti. L’obiettivo è descrivere una situazione in cui più operazioni indipendenti hanno prodotto errori.
Nel codice dell’esempio, asyncio.TaskGroup avvia tre attività. Il controllo del servizio di autenticazione termina correttamente, mentre i servizi di pagamenti e notifiche sollevano un ConnectionError. Il TaskGroup raccoglie gli errori e li presenta come un gruppo.
La sintassi except*
La forma except* è diversa dal normale except. Con except, viene gestita un’eccezione singola o l’intero oggetto ricevuto. Con except*, Python cerca all’interno di un ExceptionGroup tutte le eccezioni compatibili con il tipo indicato.
Per esempio:
try:
# Operazione che può produrre più errori
pass
except* ConnectionError as errori_connessione:
print("Gestione degli errori di rete")
except* ValueError as errori_valore:
print("Gestione dei dati non validi")
Se il gruppo contiene sia un ConnectionError sia un ValueError, i due blocchi possono essere eseguiti entrambi. Questo permette di applicare una strategia diversa a ciascuna categoria di problema.
Perché usare TaskGroup
asyncio.TaskGroup è preferibile, in molti casi, rispetto alla creazione manuale di task con asyncio.create_task(). Gestisce il ciclo di vita delle attività e assicura che le eccezioni non vengano ignorate.
Quando un’attività fallisce, il gruppo può annullare le altre attività ancora in esecuzione e attendere la loro conclusione. Al termine, le eccezioni vengono aggregate. Questo rende il comportamento del programma più prevedibile e riduce il rischio di lasciare attività “orfane” in background.
Eccezioni residue
Un blocco except* può gestire solo una parte delle eccezioni presenti nel gruppo. Quelle non gestite continuano a essere propagate. Per questo nell’esempio è presente un secondo blocco:
except* Exception as gruppo_errori:
for errore in gruppo_errori.exceptions:
print(errore)
È importante non sostituire automaticamente Exception con BaseException. Quest’ultima include eccezioni come KeyboardInterrupt e SystemExit, che normalmente devono poter interrompere il programma.
Best practice
- Usa Python 3.11 o successivo:
ExceptionGroup,except*eTaskGroupsono funzionalità moderne del linguaggio. - Raggruppa solo errori indipendenti: un gruppo è utile quando più operazioni possono fallire autonomamente. Non usarlo per nascondere una normale eccezione singola.
- Gestisci eccezioni specifiche: preferisci
except* ConnectionErrora un blocco generico. Il codice diventa più preciso e facile da manutenere. - Registra tutte le informazioni utili: nei programmi reali usa il modulo
logginginvece di affidarti soltanto aprint(). - Non ignorare gli errori: se alcune eccezioni rimangono non gestite, valuta se lasciarle propagare oppure se aggiungere una gestione esplicita.
- Progetta attività indipendenti: le operazioni eseguite in parallelo dovrebbero avere effetti controllabili anche se una di esse fallisce.
- Testa scenari multipli: verifica il caso senza errori, il fallimento di una sola attività e il fallimento simultaneo di più attività.
Un errore frequente consiste nell’usare un normale except aspettandosi che gestisca ogni elemento del gruppo. Per gli ExceptionGroup occorre utilizzare la sintassi con l’asterisco, cioè except*.
Riepilogo
ExceptionGroup consente di rappresentare più errori contemporaneamente, mentre except* permette di selezionare e gestire le eccezioni contenute nel gruppo in base al loro tipo.
Queste funzionalità sono particolarmente adatte ai programmi asincroni e concorrenti. Insieme a asyncio.TaskGroup, permettono di scrivere codice più robusto, perché gli errori delle attività parallele non vengono persi e possono essere analizzati separatamente.
La regola principale è usare questo approccio quando gli errori sono realmente indipendenti. In questo modo la gestione delle eccezioni resta chiara, prevedibile e coerente con la struttura concorrente dell’applicazione.
Approfondisci con risorse ufficiali
- Documentazione Python sulle eccezioni: descrive la gerarchia delle eccezioni e le regole generali per gestirle.
- PEP 654: presenta la motivazione e il funzionamento di
ExceptionGroupeexcept*. - Documentazione del modulo asyncio: approfondisce
TaskGroup, la cancellazione delle attività e la programmazione asincrona. - Documentazione Python su logging: mostra come registrare errori e traceback in modo adatto ad applicazioni reali.
