A la lliçó anterior vam aprendre a llegir l'avís; en aquesta aprenem a fer-nos-en càrrec. L'eina és l'estructura try/except, i amb ella Papyrus saldarà avui tres deutes concrets: carregar_cataleg() deixarà de morir quan cataleg.json no existeixi (adeu al pal·liatiu Path.exists()), el peatge str→float del CSV deixarà de "resar perquè porti números" i sobreviurà a files corruptes, i l'int(input()) que arrossega el seu ValueError des del mòdul 1 — apedaçat al mòdul 2 amb isdigit() — rebrà per fi la seva validació definitiva. Pel camí veurem les quatre peces de l'estructura completa (try, except, else, finally), com capturar l'objecte excepció amb as e, i una taula d'antipatrons encapçalada pel pitjor de tots: l'except: nu.

Contingut

  1. L'estructura try/except: demanar perdó, per fi
  2. Capturar tipus concrets (i per què mai un except: nu)
  3. Diversos except i capturar l'objecte amb as e
  4. Les peces restants: else i finally
  5. Papyrus, versió robusta: carregar_cataleg() i el CSV de vendes
  6. Saldar el deute del mòdul 1: demanar_enter()

L'estructura try/except: demanar perdó, per fi

La sintaxi mínima té dos blocs: el que intentes i el que fas si falla:

try:
    preu = float(text)               # l'intent
except ValueError:
    print(f"'{text}' no es un preu valid.")   # el pla B
    preu = 0.0

Com flueix l'execució, pas a pas:

  1. Python executa el bloc try línia a línia.
  2. Si no salta cap excepció, el bloc except s'ignora del tot i la vida continua.
  3. Si una línia del try llança una excepció, l'execució abandona el try immediatament (les línies restants del bloc no s'executen) i Python busca un except el tipus del qual coincideixi.
  4. Coincidir vol dir: l'excepció és d'aquella classe o d'una subclasse — la jerarquia de 07-01 en acció. Un except LookupError: atrapa tant KeyError com IndexError.
  5. Si cap except no coincideix, l'excepció continua propagant-se cap amunt, com si el try no existís.

El punt 5 és important: try no és cap escut màgic. Només atrapa allò que li dius que atrapi. I això és una virtut, com veurem ara mateix.

Capturar tipus concrets (i per què mai un except: nu)

La temptació del principiant és "que no falli res": embolicar-ho tot i atrapar-ho tot. És l'error més car de tot el mòdul, perquè converteix errors sorollosos en errors silenciosos — i un error silenciós pot costar diners a l'Anna sense que ningú se n'assabenti.

# ❌ L'ANTIPATRO: no facis mai aixo
try:
    cataleg = carregar_cataleg()
except:                    # atrapa TOT, fins i tot el que no hauria
    cataleg = {}           # ...i ho amaga sota la catifa

Què amaga aquest except: nu? Tot. Si hi ha una errada catalg dins de carregar_cataleg() (un NameError, un bug teu), el programa arrenca amb el catàleg buit i ningú no sabrà mai per què. Si l'Anna prem Ctrl+C per cancel·lar (KeyboardInterrupt, que hereta de BaseException i també cau en un except: nu), el programa ho ignora. El traceback — aquell informe forense gratuït de 07-01 — va directe a les escombraries.

Antipatró Per què és nociu Alternativa correcta
except: nu Atrapa fins i tot KeyboardInterrupt i SystemExit; oculta bugs propis except TipusConcret:
except Exception: com a reflex Menys greu (deixa passar el Ctrl+C) però continua barrejant bugs amb fallades esperables Capturar el tipus que saps gestionar; Exception només a la frontera del programa (07-05)
except ValueError: pass Silencia l'error sense deixar-ne cap rastre Com a mínim, avisar o comptar; idealment registrar-ho al log (07-05)
try gegant al voltant de 30 línies Impossible saber quina línia ha fallat ni si l'except gestiona el que toca try mínim: només la línia (o línies) que poden fallar d'aquella manera
Atrapar i retornar un valor "normal" (return -1) L'error es disfressa de dada i explota més lluny, on és il·legible Retornar None amb criteri (07-03) o deixar propagar

La regla que resumeix la taula: captura només allò que esperes i saps gestionar; deixa que tota la resta exploti. Una excepció no gestionada amb el seu traceback és infinitament millor que un programa que menteix.

Diversos except i capturar l'objecte amb as e

Un mateix try pot fallar de maneres diferents que mereixen reaccions diferents. S'encadenen diversos except, i Python fa servir el primer que coincideixi (per això els tipus específics van abans que els genèrics):

try:
    with open(ruta, encoding="utf-8") as f:
        dades = json.load(f)
except FileNotFoundError:
    print("No hi ha cataleg previ. Es partira d'un de buit.")
    dades = []
except json.JSONDecodeError as e:
    print(f"El cataleg esta corrupte ({e}). Revisa el fitxer o restaura una copia.")
    raise SystemExit(1)

Dues novetats aquí:

  • as e guarda l'objecte excepció a la variable e, disponible dins del bloc. Recorda de 07-01 que l'excepció és un objecte: print(e) en mostra el missatge, i alguns tipus porten atributs extres (e.lineno a JSONDecodeError diu a quina línia del JSON hi ha la destrossa). La variable només existeix dins del seu bloc except.
  • json.JSONDecodeError és la resposta a una altra pregunta del tancament de M6: "i si la còpia és corrupta?". Si algú va editar malament cataleg.json a mà — el mòdul 6 només va poder suplicar "no editis malament el JSON" —, json.load() llança aquesta excepció, i ara sabem distingir fitxer absent (recuperable: catàleg buit) de fitxer corrupte (no t'inventis dades: avisa i atura't).

Si diverses excepcions mereixen la mateixa reacció, s'agrupen en una tupla: except (ValueError, KeyError) as e:.

Les peces restants: else i finally

L'estructura completa té quatre blocs, cadascun amb un paper precís:

try:
    f = open(ruta, encoding="utf-8")     # pot fallar
except FileNotFoundError:
    print("Fitxer no trobat.")           # pla B
else:
    contingut = f.read()                 # nomes si el try HA ANAT BE
    f.close()
finally:
    print("Fi de l'intent de lectura.")  # SEMPRE, passi el que passi
Bloc S'executa quan... Per a què serveix
try Sempre (és l'intent) La(les) operació(ns) que poden fallar
except Només si el try ha llançat una excepció coincident El pla B: recuperar-se, avisar, registrar
else Només si el try ha acabat sense excepció La feina que depèn de l'èxit, fora del try per no atrapar-ne els errors per accident
finally Sempre: amb èxit, amb excepció gestionada, amb excepció no gestionada i fins i tot amb un return pel mig Neteja garantida: tancar, alliberar, deixar-ho tot en ordre

Per què molestar-se amb else en comptes d'escriure-ho tot dins del try? Precisió: si f.read() fos dins del try i fallés per un altre motiu, un except massa ample podria atrapar-lo i diagnosticar malament. El try ha d'abrigar només les línies la fallada de les quals saps gestionar; else recull la continuació del camí feliç.

I finally? Et sonarà la garantia: és exactament el que el with del mòdul 6 fa per tu amb els fitxers — tancar passi el que passi. De fet, with està construït sobre aquesta mecànica, i al mòdul 8 aprendrem a fabricar els nostres propis context managers. Mentrestant, la parella pràctica és: with per a fitxers, finally per a qualsevol altra neteja que hagi de passar sí o sí (imprimir un resum de tancament, restaurar un estat).

flowchart TD
    A[try] -->|sense excepció| B[else]
    A -->|excepció coincident| C[except]
    A -->|excepció NO coincident| D[es propaga cap amunt]
    B --> E[finally]
    C --> E
    D --> E
    E -->|si venia propagant-se| F[continua propagant-se]

Papyrus, versió robusta: carregar_cataleg() i el CSV de vendes

És hora de pagar els deutes del mòdul 6. Així va quedar carregar_cataleg() aleshores, pal·liatiu inclòs:

# Versio M6 — amb pal.liatiu (LBYL)
def carregar_cataleg():
    ruta = BASE / "dades" / "cataleg.json"
    if not ruta.exists():                      # LBYL: foto del passat
        return {}
    with open(ruta, encoding="utf-8") as f:
        llista = json.load(f)
    return {l["titol"].strip().casefold(): Llibre(**l) for l in llista}

I així queda la versió EAFP, que a més distingeix absent de corrupte:

# Versio M7 — EAFP
def carregar_cataleg():
    ruta = BASE / "dades" / "cataleg.json"
    try:
        with open(ruta, encoding="utf-8") as f:
            llista = json.load(f)
    except FileNotFoundError:
        print("Avis: no existeix cataleg.json. Es parteix d'un cataleg buit.")
        return {}
    except json.JSONDecodeError as e:
        print(f"Error: cataleg.json esta corrupte ({e}).")
        print("Restaura una copia de copies/ abans de continuar.")
        raise SystemExit(1)
    return {l["titol"].strip().casefold(): Llibre(**l) for l in llista}

Punt per punt:

  • El try cobreix només l'obertura i el parseig — les dues operacions que poden fallar d'aquestes maneres. La construcció del diccionari queda fora: si Llibre(**l) fallés (un preu negatiu al JSON dispararia el ValueError del __post_init__ de M5), volem veure aquell traceback, no confondre'l amb un problema de fitxer.
  • FileNotFoundError és un cas esperat i recuperable: primera arrencada en un ordinador nou. Catàleg buit + avís honest. Sense condició de carrera: l'open() és la comprovació.
  • JSONDecodeError és esperat però no recuperable en silenci: inventar-se un catàleg buit quan n'existeix un de corrupte faria que l'Anna aixafés les seves dades reals en desar. Avisem amb el detall de l'error i parem. Gestionar bé una excepció de vegades vol dir aturar-se millor, no continuar a qualsevol preu.

Segon deute: el peatge str→float de vendes.csv (06-02). Si una fila porta "gratis" com a quantia, la versió M6 mor en plena suma. La M7 salta la fila corrupta, compta els errors i continua:

def tancament_de_caixa():
    ruta = BASE / "dades" / "vendes.csv"
    total, vendes_ok, files_corruptes = 0.0, 0, 0
    try:
        with open(ruta, encoding="utf-8", newline="") as f:
            for fila in csv.DictReader(f):
                try:
                    total += float(fila["quantia"])   # el peatge str→float
                except ValueError:
                    files_corruptes += 1
                    print(f"  Fila ignorada, quantia no llegible: {fila!r}")
                else:
                    vendes_ok += 1
    except FileNotFoundError:
        print("Avui no hi ha vendes registrades.")
        return 0.0
    if files_corruptes:
        print(f"Atencio: {files_corruptes} fila/es corrupta/es a vendes.csv.")
    print(f"Tancament de caixa: {vendes_ok} vendes, total {total:.2f} €")
    return total

Observa el try niat i mínim: l'interior abriga una sola línia (la conversió) i atrapa un sol tipus (ValueError). Una fila dolenta ja no tomba el tancament del dia — però tampoc no desapareix: es compta i es mostra, perquè silenciar-la seria l'antipatró pass de la taula. L'else interior suma la venda al comptador només quan la conversió ha anat bé: exactament el paper que li hem assignat a else.

Saldar el deute del mòdul 1: demanar_enter()

El deute més antic del curs. Al mòdul 1, int(input()) explotava si la Júlia teclejava "tres". Al mòdul 2 ho vam apedaçar amb while True + isdigit(), i ja aleshores vam avisar que era provisional. Per què? Perquè isdigit() intenta predir el que int() acceptarà, i prediu malament: "-2".isdigit() és False (rebutja negatius legítims), " 4 ".isdigit() és False (rebutja espais que int() tolera). És LBYL amb un validador que no coincideix amb el conversor. La solució EAFP: que decideixi el mateix int().

def demanar_enter(missatge):
    """Demana un enter per teclat fins que l'entrada sigui valida."""
    while True:
        text = input(missatge)
        try:
            return int(text)           # si converteix, retornem i sortim
        except ValueError:
            print(f"'{text}' no es un nombre enter. Torna-ho a provar.")


unitats = demanar_enter("Quants exemplars de Hamlet? ")
  • El return dins del try trenca el bucle tan bon punt hi ha un enter vàlid — no cal break.
  • Si int() llança ValueError, l'except informa (mostrant el que s'ha teclejat, missatge accionable) i el while True torna a intentar-ho.
  • El jutge és el mateix int(): accepta "-2" i " 4 ", rebutja "tres" i "4.5". Zero desacords entre validador i conversor, perquè són la mateixa cosa.

La mateixa plantilla serveix per a demanar_preu() amb float(): guarda-la, perquè és una de les funcions més reutilitzades que escriuràs.

Errors Comuns i Consells

  • except: nu. L'antipatró número u. Atrapa fins i tot el Ctrl+C de l'Anna i enterra els teus propis bugs. Sempre un tipus concret.
  • try quilomètric. Si el bloc té deu línies, ja no saps quina ha fallat ni si el teu except és la reacció adequada. Encongeix el try fins a la línia culpable; fes servir else per a la continuació.
  • Silenciar amb pass. except ValueError: pass fa desaparèixer dades sense deixar rastre. A tancament_de_caixa() les files corruptes es compten i es mostren; a 07-05 aniran al log.
  • Ordenar malament els except. Python fa servir el primer que coincideix: si poses except Exception: abans que except ValueError:, el segon és codi mort. De l'específic al general.
  • Oblidar que else existeix. No és obligatori, però ficar el camí feliç dins del try "perquè hi cap" acaba atrapant excepcions que no volies atrapar.
  • Consell: abans d'escriure un except, pregunta't: "què faré amb aquest error?". Si la resposta és "res", probablement no l'hauries d'atrapar: deixa que es propagui cap a qui sí que hi pugui fer alguna cosa.
  • Consell: fixa't que els nostres missatges d'avís sempre inclouen la dada culpable ('{text}', {fila!r}). Un avís sense la dada obliga a endevinar; amb la dada, s'arregla en un minut.

Exercicis

  1. demanar_preu(). Escriu la bessona de demanar_enter(): demana un preu amb float(), torna a intentar-ho davant de ValueError i, a més, rebutja preus negatius amb un missatge diferent ("un preu no pot ser negatiu") sense sortir del bucle. Prova-la amb "9,95" (coma decimal, molt habitual aquí): què passa i per què?

  2. carregar_config() a prova de tot. El mòdul 6 va deixar config.json amb {"nom_botiga", "iva_llibres", "descompte_soci"}. Escriu carregar_config() que: retorni el diccionari si tot va bé; davant de FileNotFoundError retorni la configuració per defecte {"nom_botiga": "Papyrus", "iva_llibres": 0.04, "descompte_soci": 0.05} amb un avís; i davant de json.JSONDecodeError mostri l'error i rellanci SystemExit(1). Afegeix-hi un finally que imprimeixi sempre "Configuracio: proces acabat." i comprova que apareix en els tres escenaris.

  3. Caçar l'antipatró. Aquest codi "funciona", però comet com a mínim tres pecats de la taula d'antipatrons. Identifica'ls i reescriu-lo:

    def preu_soci(cataleg, titol):
        try:
            llibre = cataleg[titol.strip().casefold()]
            preu = llibre.preu * (1 - DESCOMPTE_SOCI)
            print("El preu per a socis de " + titol + " es " + str(round(preu, 2)))
            return preu
        except:
            return -1
    

Solucions

  1. def demanar_preu(missatge):
        while True:
            text = input(missatge)
            try:
                preu = float(text)
            except ValueError:
                print(f"'{text}' no es un numero. Fes servir punt decimal, p. ex. 9.95")
                continue
            if preu < 0:
                print("Un preu no pot ser negatiu. Torna-ho a provar.")
                continue
            return preu
    

    Amb "9,95" salta ValueError: float() només entén el punt decimal. Per això el missatge de l'except suggereix el format correcte — missatge accionable. (Alternativa amable: text.replace(",", ".") abans de convertir.) Fixa't en l'ordre: primer la conversió (EAFP), després la validació del valor ja convertit (LBYL sobre una dada que ja tens a la mà, legítim segons vam veure a 07-01).

  2. CONFIG_DEFECTE = {"nom_botiga": "Papyrus", "iva_llibres": 0.04, "descompte_soci": 0.05}
    
    def carregar_config():
        ruta = BASE / "dades" / "config.json"
        try:
            with open(ruta, encoding="utf-8") as f:
                return json.load(f)
        except FileNotFoundError:
            print("Avis: sense config.json; es fan servir els valors per defecte.")
            return dict(CONFIG_DEFECTE)
        except json.JSONDecodeError as e:
            print(f"config.json corrupte: {e}")
            raise SystemExit(1)
        finally:
            print("Configuracio: proces acabat.")
    

    El finally s'imprimeix en els tres escenaris — fins i tot amb els return pel mig i fins i tot quan SystemExit s'està propagant. Aquesta és la seva garantia. Detall fi: es retorna dict(CONFIG_DEFECTE) (una còpia) perquè ningú no muti el diccionari per defecte compartit, record de l'aliasing del mòdul 4.

  3. Pecats: (1) except: nu — atraparia fins i tot un NameError si DESCOMPTE_SOCI no estigués definit, ocultant un bug; (2) try massa ample — només l'accés cataleg[...] pot fallar de manera esperable (KeyError); el print i l'aritmètica no haurien d'estar abrigats; (3) retornar -1 com a error disfressat de preu — qui rebi aquest -1 podria cobrar-lo a la Júlia. Reescriptura:

    def preu_soci(cataleg, titol):
        try:
            llibre = cataleg[titol.strip().casefold()]
        except KeyError:
            print(f"'{titol}' no es al cataleg.")
            return None
        return round(llibre.preu * (1 - DESCOMPTE_SOCI), 2)
    

    I és None el millor senyal de "no trobat"? O aquesta funció hauria de llançar la seva pròpia excepció? Aquesta pregunta exacta — quan retornar None i quan fer raise — obre la propera lliçó.

Conclusió

Ja saps demanar perdó amb estil: try mínim al voltant de la línia que pot fallar, except de tipus concrets (mai nu), diversos except ordenats de l'específic al general, as e per aprofitar el missatge de l'objecte excepció, else per al camí feliç que depèn de l'èxit i finally per a la neteja incondicional. Papyrus ha pagat tres deutes: carregar_cataleg() distingeix "no hi ha catàleg" (recuperable) de "catàleg corrupte" (millor parar), tancament_de_caixa() sobreviu a files il·legibles comptant-les en comptes de morir o silenciar-les, i demanar_enter() jubila l'impostor isdigit() deixant que int() sigui el seu propi jutge. Fins ara només hem reaccionat a excepcions que Python llança pel seu compte. La propera lliçó inverteix els papers: aprendràs a llançar-les tu amb raise — decidir quan una situació mereix excepció i quan un simple None, rellançar, encadenar amb from, i protegir les teves funcions amb guard clauses. El raise ValueError que el __post_init__ de Llibre fa des del mòdul 5 deixarà de ser un acte de fe per convertir-se en una decisió de disseny.

Curs de Programació en Python

Mòdul 1: Introducció a Python

Mòdul 2: Estructures de Control

Mòdul 3: Funcions i Mòduls

Mòdul 4: Estructures de Dades

Mòdul 5: Programació Orientada a Objectes

Mòdul 6: Gestió de Fitxers

Mòdul 7: Gestió d'Errors i Excepcions

Mòdul 8: Temes Avançats

Mòdul 9: Proves i Depuració

Mòdul 10: Desenvolupament Web amb Python

Mòdul 11: Ciència de Dades amb Python

Mòdul 12: Projecte Final

© Copyright 2026. Tots els drets reservats