Python e gestione delle eccezioni: propagare, rilanciare e arricchire gli errori

by theArchitect
SHARE
Python e gestione delle eccezioni: propagare, rilanciare e arricchire gli errori
© Guida-HTML5.it

Introduzione

Quando si parla di gestione delle eccezioni in Python, molti tutorial si fermano a try e except. In pratica, però, uno degli aspetti più utili e spesso sottovalutati è come propagare correttamente un errore verso livelli superiori, oppure come rilanciarlo dopo aver aggiunto contesto utile al debug.

Questo è fondamentale in applicazioni reali: API, script di automazione, servizi backend e tool da riga di comando. Gestire male un errore significa perdere informazioni importanti; gestirlo bene significa rendere il codice più leggibile, più affidabile e molto più facile da mantenere.

In questo tutorial vedremo un sotto-argomento pratico e molto utile: rilanciare eccezioni con contesto aggiuntivo usando raise ... from ... e la propagazione consapevole degli errori. È un tema diverso da quelli già elencati e ti aiuterà a scrivere codice più professionale.

Codice completo

class ConfigError(Exception):
    """Errore generico legato alla configurazione dell´applicazione."""


class DatabaseConnectionError(Exception):
    """Errore di connessione al database."""


def load_config(path: str) -> dict:
    """
    Carica una configurazione da file.
    In questo esempio simuliamo un errore di parsing.
    """
    if not path.endswith(".json"):
        raise ConfigError(f"Formato non supportato per il file: {path}")

    # Simulazione di un file corrotto o non valido
    raise ValueError("JSON non valido: carattere inatteso alla riga 3")


def connect_to_database(config: dict) -> str:
    """
    Simula una connessione al database.
    """
    host = config.get("db_host")
    if not host:
        raise DatabaseConnectionError("Parametro ´db_host´ mancante nella configurazione")

    # Simulazione di un errore di rete
    raise ConnectionError(f"Impossibile raggiungere il server: {host}")


def start_application(config_path: str) -> None:
    """
    Avvia l´applicazione caricando la configurazione e connettendosi al database.
    L´obiettivo è arricchire gli errori con contesto utile.
    """
    try:
        config = load_config(config_path)
    except ValueError as exc:
        raise ConfigError(
            f"Errore durante il caricamento della configurazione da ´{config_path}´"
        ) from exc

    try:
        connect_to_database(config)
    except ConnectionError as exc:
        raise DatabaseConnectionError(
            "Connessione al database fallita durante l´avvio dell´applicazione"
        ) from exc


if __name__ == "__main__":
    try:
        start_application("settings.json")
    except ConfigError as exc:
        print(f"[CONFIG] {exc}")
        if exc.__cause__ is not None:
            print(f"  Causa originale: {exc.__cause__}")
    except DatabaseConnectionError as exc:
        print(f"[DB] {exc}")
        if exc.__cause__ is not None:
            print(f"  Causa originale: {exc.__cause__}")

Spiegazione

Il codice mostra un flusso tipico di un´applicazione: prima si carica la configurazione, poi si prova a connettersi a un database. In entrambi i passaggi possono verificarsi errori diversi.

1. Eccezioni personalizzate per dare significato al problema

Abbiamo definito due eccezioni dedicate:

  • ConfigError per i problemi di configurazione
  • DatabaseConnectionError per i problemi di connessione

Usare eccezioni custom non serve solo a “fare ordine”: permette anche di distinguere con precisione il tipo di guasto, sia per chi legge il codice sia per chi lo gestisce più in alto.

2. Non perdere il contesto originale

Nel blocco:

except ValueError as exc:
    raise ConfigError(...) from exc

stiamo intercettando un errore tecnico di basso livello, in questo caso un ValueError, e lo trasformiamo in un errore più significativo per il dominio dell’applicazione. Il punto importante è la parte from exc.

Questa sintassi dice a Python: “questa nuova eccezione è causata da quella precedente”. In questo modo, quando l’errore viene stampato o ispezionato, non perdiamo la causa originale. È molto utile per il debug, perché ci permette di vedere sia il contesto applicativo sia il dettaglio tecnico.

3. La differenza tra propagare e nascondere un errore

Molti principianti scrivono blocchi come questo:

try:
    ...
except Exception:
    print("Errore generico")

Il problema è che così si nasconde l’errore. Il programma magari continua, ma senza informazioni utili. In un progetto reale questo è rischioso, perché potresti ritrovarti con bug difficili da diagnosticare.

Invece, se un errore non puoi gestirlo davvero, spesso è meglio rilanciarlo con più contesto o lasciarlo propagare fino al livello che sa come reagire.

4. Il ruolo di __cause__

Nell’ultima parte del codice stampiamo:

if exc.__cause__ is not None:
    print(f"  Causa originale: {exc.__cause__}")

Questo mostra l’eccezione originale che ha generato il problema. Non sempre è necessario accedere direttamente a __cause__, ma sapere che esiste è molto utile durante il debug o nella costruzione di log strutturati.

5. Cosa succede senza from exc

Se scrivessimo semplicemente:

raise ConfigError("Errore nel caricamento della configurazione")

perderemmo il collegamento esplicito con l’errore originale. Python manterrebbe comunque una traccia interna, ma from exc rende la relazione chiara e intenzionale. È una best practice quando stai traducendo un errore tecnico in un errore di dominio.

Best practice

  • Non catturare eccezioni troppo generiche se non hai un motivo preciso. Preferisci il tipo specifico che sai gestire.
  • Rilancia con contesto quando un errore di basso livello deve diventare più comprensibile per il resto dell’applicazione.
  • Usa eccezioni custom per separare i problemi di dominio dagli errori tecnici interni.
  • Non nascondere la causa originale: raise ... from ... è spesso la scelta migliore.
  • Non stampare e basta in caso di errore critico: se il programma non può continuare, l’eccezione deve arrivare a chi può decidere cosa fare.
  • Fai logging nei livelli alti dell’applicazione, non ovunque. Il codice di basso livello dovrebbe soprattutto segnalare il problema in modo chiaro.
  • Scrivi messaggi di errore utili: indica sempre quale operazione è fallita, su quale input o risorsa, e perché se possibile.

Riepilogo

In questo tutorial abbiamo visto un aspetto molto pratico della gestione delle eccezioni in Python: come propagare e rilanciare errori senza perdere informazioni importanti. La sintassi raise ... from ... è uno strumento semplice ma potentissimo, perché collega l’errore applicativo alla sua causa originale.

Questo approccio è particolarmente utile quando:

  • stai traducendo errori tecnici in errori di dominio
  • vuoi mantenere il traceback leggibile
  • stai costruendo un’applicazione modulare con più livelli
  • vuoi migliorare il debug e la manutenzione del codice

In sintesi: non limitarti a intercettare le eccezioni. Impara a gestirle in modo intelligente, lasciando che trasportino il contesto necessario a capire davvero cosa è successo.

Approfondisci con risorse ufficiali

  • Documentazione Python sulle eccezioni: la guida ufficiale spiega sintassi, gerarchia e comportamento delle eccezioni.
  • PEP 3134: introduce le eccezioni concatenate e il concetto di causa esplicita con raise ... from ....
  • Built-in Exceptions: elenco completo delle eccezioni standard disponibili in Python.
  • Logging module: utile per registrare errori e contesto in applicazioni reali.

Se vuoi, nel prossimo passo posso scrivere anche un tutorial complementare su come usare il logging insieme alle eccezioni in Python, con un esempio realistico da progetto backend.

SHARE