El mòdul 6 va acabar amb una llista incòmoda de "pal·liatius": Path.exists() abans de llegir, "no editis malament el JSON", "resa perquè el CSV porti números". Papyrus ja té memòria — catàleg, vendes, socis i còpies de seguretat viuen a dades/ — però aquesta memòria és fràgil: si cataleg.json no és on hauria de ser, si una fila del CSV porta "gratis" a la columna del preu, o si la Júlia tecleja un número impossible, el programa mor amb un bloc de text vermell a mitja pantalla. Aquest bloc de text té nom (traceback), aquest "morir" té mecànica (una excepció que ningú no atrapa), i tot aquest mòdul tracta de dominar totes dues coses. En aquesta primera lliçó encara no arreglarem res: entendrem què és exactament una excepció, aprendrem a llegir un traceback com qui llegeix un informe d'incidències, coneixerem les excepcions integrades que Papyrus ja ha patit sense saber-ne el nom, i assumirem la filosofia que Python prefereix: demanar perdó en comptes de demanar permís.

Contingut

  1. Què és una excepció: el mecanisme, no el desastre
  2. Anatomia d'un traceback: llegir-lo de baix a dalt
  3. Errors de sintaxi vs excepcions en temps d'execució
  4. Les excepcions integrades que Papyrus ja coneix (sense saber-ho)
  5. La jerarquia d'excepcions: Exception com a mare
  6. EAFP vs LBYL: per què Path.exists() era un pal·liatiu

Què és una excepció: el mecanisme, no el desastre

Una excepció és un objecte que Python crea quan una operació no es pot completar, i que interromp el flux normal del programa per viatjar cap amunt buscant algú que se'n faci càrrec. Tres idees clau:

  • És un objecte. Igual que Llibre("Hamlet", 9.95, 6) és una instància de la classe Llibre, un ValueError("el preu no pot ser negatiu") és una instància de la classe ValueError. Porta a dins un missatge i, com veurem a 07-04, pot portar atributs propis.
  • Es propaga. Quan passa, l'execució abandona la línia actual, surt de la funció, surt de la funció que l'havia cridat, i així successivament. Aquest viatge ascendent es diu propagació i és el que fa que un error a la línia 3 d'una funció auxiliar acabi apareixent com a fallada de tot el programa.
  • Si ningú no l'atrapa, el programa acaba. Python imprimeix el traceback i retorna el control al sistema operatiu. Això és el que l'Anna ha estat veient fins ara: excepcions no gestionades.

Fixa't en el matís: l'excepció no és el problema. L'excepció és l'avís estructurat que hi ha hagut un problema. El desastre és no tenir ningú escoltant l'avís.

# fitxa_llibre.py — el mateix codi innocent del modul 1
preu_text = input("Preu del llibre: ")     # Julia tecleja: gratis
preu = float(preu_text)                    # aqui neix l'excepcio
print(f"Amb IVA: {preu * 1.04:.2f} €")     # aquesta linia no s'executa mai

Quan float("gratis") no pot produir cap número, Python construeix un objecte ValueError, abandona la línia 3 i, com que ningú no l'atrapa, el programa acaba. La línia del print no arriba a executar-se mai: la propagació talla el flux en sec.

Anatomia d'un traceback: llegir-lo de baix a dalt

Això és el que imprimeix Python quan mor l'exemple anterior:

Traceback (most recent call last):
  File "fitxa_llibre.py", line 2, in <module>
    preu = float(preu_text)
ValueError: could not convert string to float: 'gratis'

I aquest és un cas real amb diverses funcions encadenades, a l'estil de caixa.py del mòdul 6:

Traceback (most recent call last):
  File "caixa.py", line 42, in <module>
    cataleg = carregar_cataleg()
  File "caixa.py", line 18, in carregar_cataleg
    with open(BASE / "dades" / "cataleg.json", encoding="utf-8") as f:
FileNotFoundError: [Errno 2] No such file or directory: 'dades/cataleg.json'

La regla d'or per llegir-lo: comença per baix.

  1. Última línia: el tipus d'excepció (FileNotFoundError) i el seu missatge. És el què. Llegeix-la sempre primer.
  2. Penúltima entrada (fitxer + línia + codi): el lloc exacte on va néixer l'excepció. És el on.
  3. D'allà cap amunt: la cadena de crides que hi va portar (<module> va cridar carregar_cataleg, que va executar l'open). És el com s'hi va arribar. most recent call last vol dir literalment això: la crida més recent és al final.
Part del traceback Pregunta que respon A l'exemple
Última línia (Tipus: missatge) Què ha fallat? No existeix dades/cataleg.json
Última entrada File ... line ... On ha nascut? Línia 18, dins de carregar_cataleg
Entrades superiors Qui ha cridat a qui? El programa principal (línia 42) ha cridat carregar_cataleg

Un traceback no és cap càstig: és el millor informe d'incidències gratuït que existeix. Aprendre a llegir-lo amb calma t'estalviarà hores; copiar-lo sencer (última línia inclosa) és també la manera correcta de demanar ajuda.

Errors de sintaxi vs excepcions en temps d'execució

No tot el text vermell és igual. Hi ha dues famílies radicalment diferents:

# SyntaxError: Python ni tan sols comenca a executar
if preu > 0
    print("preu valid")
  File "caixa.py", line 1
    if preu > 0
               ^
SyntaxError: expected ':'
Error de sintaxi (SyntaxError) Excepció en execució
Quan apareix Abans d'executar res: en compilar el fitxer En plena execució, en arribar a la línia problemàtica
Depèn de les dades No: falla sempre, amb qualsevol entrada Sí: float(text) funciona amb "12.50" i explota amb "gratis"
Es pot gestionar amb el mecanisme d'aquest mòdul? No: es corregeix el codi i punt Sí: és exactament per a això que serveixen try/except (07-02)
Exemple Papyrus Oblidar els dos punts d'un if El CSV de vendes porta una fila corrupta

La conseqüència pràctica és important: un SyntaxError és sempre un bug teu i s'arregla editant; una excepció en execució pot ser un bug o pot ser el món real comportant-se com el món real (fitxers que no hi són, usuaris que teclegen qualsevol cosa). Aquest mòdul tracta la segona família.

Les excepcions integrades que Papyrus ja coneix (sense saber-ho)

Python porta desenes d'excepcions predefinides. Aquestes set cobreixen el 90 % del dia a dia, i totes tenen ja un episodi a la història de Papyrus:

Excepció Quan salta Exemple Papyrus
ValueError El tipus és correcte però el valor no val float("gratis") en llegir vendes.csv; el __post_init__ de Llibre amb preu=-5 (M5)
TypeError El tipus de dada no és l'esperat per a l'operació "12.50" * 0.04 no falla (repeteix text), però "12.50" + 0.04 sí: no se suma str amb float
KeyError Clau inexistent en un diccionari cataleg["faust "] — l'espai final trenca la clau normalitzada amb strip().casefold() (M4)
IndexError Índex fora de rang en una llista o tupla darreres_vendes[10] quan avui només hi ha hagut 3 vendes
FileNotFoundError S'intenta obrir un fitxer que no existeix open(BASE / "dades" / "cataleg.json") al portàtil nou de l'Anna, on dades/ encara no existeix (M6)
ZeroDivisionError Divisió entre zero total_vendes / nombre_vendes a tancament_de_caixa() un dia sense vendes
AttributeError L'objecte no té aquest atribut o mètode llibre.titul (errada) en comptes de llibre.titol; o cridar .append() sobre el dict del catàleg

Val la pena provocar-les a propòsit a l'intèrpret interactiu: tecleja int("Hamlet"), {}["faust"], [][0]... Veure el nom i el missatge de cadascuna en un entorn controlat fa que les reconeguis a l'instant quan apareguin en un traceback real.

>>> cataleg = {"hamlet": "Llibre(Hamlet, 9.95, 6)"}
>>> cataleg["Hamlet "]        # hem oblidat normalitzar la clau
Traceback (most recent call last):
  ...
KeyError: 'Hamlet '

El missatge del KeyError mostra la clau buscada amb les seves cometes: allà es veu l'espai traïdor. Un altre motiu per llegir l'última línia amb lupa.

La jerarquia d'excepcions: Exception com a mare

Les excepcions són classes, i com tota classe de Python (ho vam veure al mòdul 5) poden heretar. Gairebé totes les que t'interessen descendeixen d'Exception:

graph TD
    BE[BaseException] --> KI[KeyboardInterrupt<br/>Ctrl+C]
    BE --> EX[Exception]
    EX --> AE[ArithmeticError] --> ZD[ZeroDivisionError]
    EX --> LE[LookupError] --> KE[KeyError]
    LE --> IE[IndexError]
    EX --> VE[ValueError]
    EX --> TE[TypeError]
    EX --> ATE[AttributeError]
    EX --> OS[OSError] --> FNF[FileNotFoundError]

Idees que convé retenir ja (en traurem partit a 07-02 i 07-04):

  • Exception és la mare dels errors "normals": els que un programa pot gestionar de manera raonable.
  • BaseException és per sobre i inclou coses que gairebé mai no has d'atrapar, com KeyboardInterrupt (el Ctrl+C amb què l'Anna talla el programa a propòsit). Per això "atrapar-ho tot" és mala idea: podries impedir fins i tot que l'usuari cancel·li.
  • Les categories intermèdies agrupen: KeyError i IndexError són totes dues LookupError ("he buscat una cosa i no hi era"); FileNotFoundError és un OSError (problema amb el sistema operatiu). Gestionar la categoria gestiona totes les seves filles — el mateix polimorfisme del mòdul 5, aplicat a errors.
  • A 07-04 estendrem aquest arbre amb excepcions pròpies de Papyrus, penjant d'Exception exactament igual que ValueError.

EAFP vs LBYL: per què Path.exists() era un pal·liatiu

El mòdul 6 en va dir pal·liatiu una vegada i una altra. Ara ho podem explicar amb vocabulari tècnic. Hi ha dues filosofies per tractar amb operacions que poden fallar:

  • LBYLLook Before You Leap ("mira abans de saltar"): comprovar les precondicions abans d'actuar. if ruta.exists(): open(ruta). És el que vam fer al mòdul 6.
  • EAFPEasier to Ask Forgiveness than Permission ("és més fàcil demanar perdó que permís"): actuar directament i gestionar l'excepció si alguna cosa surt malament. És l'estil idiomàtic de Python.
Criteri LBYL (if ruta.exists():) EAFP (intentar i gestionar)
Condició de carrera Entre l'exists() i l'open() passa temps: un altre procés (la còpia nocturna!) pot esborrar o moure el fitxer entremig. La comprovació no garanteix res L'open() és la mateixa comprovació: o funciona o avisa, sense forat intermedi
Doble feina El sistema busca el fitxer dues vegades: una per a exists(), una altra per a open() Una sola operació
Cobertura exists() només cobreix "no existeix". I si existeix però és corrupte, o sense permisos de lectura, o és un directori? Necessitaries un if per cada cas L'excepció cobreix qualsevol fallada, fins i tot les que no havies anticipat
Llegibilitat El camí feliç queda enterrat entre comprovacions El camí feliç es llegeix net; la gestió d'errors va a part

La condició de carrera (race condition) mereix un exemple concret: la còpia nocturna de Papyrus (copia_de_seguretat(), M6) reanomena i mou fitxers a dades/. Si caixa.py comprova cataleg_json.exists()True, i una dècima de segon després el procés de còpia mou aquell fitxer, l'open() posterior explota malgrat la comprovació. LBYL dona una falsa sensació de seguretat: has verificat una foto del passat, no el present.

Compte: EAFP no vol dir "no validis mai". Validar dades d'entrada (és negatiu aquest preu que ha teclejat la Júlia?) continua sent correcte i ho refinarem a 07-03. El que EAFP desaconsella és intentar predir l'estat del món exterior (fitxers, xarxa, altres processos) amb comprovacions prèvies, quan el mateix intent ja et diu la veritat.

El que encara no sabem fer és "demanar perdó": gestionar l'excepció quan arriba. Aquest és exactament el tema de la propera lliçó.

Errors Comuns i Consells

  • Llegir el traceback de dalt a baix i espantar-se a la segona línia. Al revés: última línia primer (tipus i missatge), després el punt d'origen, després la cadena de crides. De baix a dalt, sempre.
  • Confondre SyntaxError amb una excepció gestionable. Si l'error apareix abans que s'executi res, és sintaxi: es corregeix el codi, no es "gestiona".
  • Pensar que KeyError: 'Hamlet ' i KeyError: 'hamlet' són el mateix error. El missatge mostra la clau exacta entre cometes: espais i majúscules inclosos. A Papyrus, gairebé sempre vol dir que algú s'ha saltat la normalització strip().casefold().
  • Creure que Path.exists() "arregla" el problema del fitxer absent. Només cobreix una fallada entre moltes i deixa una finestra temporal oberta. És un pal·liatiu — el mòdul 6 ho va dir, i ara saps per què.
  • Consell: quan un traceback et desconcerti, reprodueix l'excepció a mà a l'intèrpret amb dades mínimes (float("gratis"), {}["x"]). Aïllar la fallada del programa gran és la meitat del diagnòstic.
  • Consell: guarda els tracebacks que et trobis aquests dies. A 07-05 aprendràs a enviar-los automàticament a un fitxer de log en comptes de perdre'ls en tancar el terminal.

Exercicis

  1. Detectiu de tracebacks. Sense executar res, prediu quina excepció (tipus exacte) llança cada fragment i per què: (a) int("12.50"); (b) cataleg = {} seguit de cataleg["hamlet"].preu; (c) vendes = [] seguit de vendes[0]; (d) "Faust" + 21.00. Després comprova-ho a l'intèrpret i compara el missatge real amb la teva predicció.

  2. Autòpsia d'un traceback. Donat aquest traceback, respon: què ha fallat?, a quina línia i funció ha nascut?, quina ha estat la cadena de crides completa?, quina dada ha provocat la fallada?

    Traceback (most recent call last):
      File "caixa.py", line 31, in <module>
        total = tancament_de_caixa()
      File "caixa.py", line 22, in tancament_de_caixa
        quanties.append(float(fila["quantia"]))
    ValueError: could not convert string to float: 'dotze amb cinquanta'
    
  3. Classificar filosofies. Per a cada situació de Papyrus, decideix si hi encaixa millor LBYL o EAFP i justifica-ho en una frase: (a) obrir dades/config.json en arrencar; (b) comprovar que unitats és més gran que 0 abans de vendre; (c) accedir a cataleg[clau] amb una clau teclejada per la Júlia; (d) comprovar que dades/ existeix abans de crear-la amb mkdir.

Solucions

  1. (a) ValueError: el tipus (str) és acceptable per a int(), però el valor "12.50" no representa un enter — int() no trunca cadenes decimals. (b) KeyError: 'hamlet': el diccionari és buit; fixa't que la fallada salta a l'accés [...], abans d'arribar tan sols a .preu. (c) IndexError: llista buida, no hi ha posició 0. (d) TypeError: no es pot concatenar str amb float; el missatge real diu can only concatenate str (not "float") to str.
  2. Què: un ValueError perquè float() ha rebut 'dotze amb cinquanta', que no és convertible a número. On: línia 22, dins de tancament_de_caixa. Cadena: el programa principal (<module>, línia 31) ha cridat tancament_de_caixa(), que a la seva línia 22 ha intentat convertir el camp quantia d'una fila del CSV. Dada culpable: algú va escriure la quantia amb lletres a vendes.csv — exactament l'escenari "resa perquè el CSV porti números" del mòdul 6.
  3. (a) EAFP: és estat del món exterior; intentar obrir i gestionar FileNotFoundError evita la condició de carrera i cobreix més fallades que exists(). (b) LBYL (validació d'entrada): és una dada que ja tens a la mà; comprovar-la abans és barat, sense finestra temporal — a 07-03 veurem que la reacció correcta a aquesta comprovació és raise. (c) EAFP: try amb accés directe (o .get(), que vam veure a M4) és més pythònic que if clau in cataleg seguit de l'accés — dues cerques en comptes d'una. (d) Truc: ni una cosa ni l'altra — ruta.mkdir(exist_ok=True) (M6) elimina el problema d'arrel; la millor comprovació és la que no cal fer.

Conclusió

Ja tens el mapa del territori: una excepció és un objecte que interromp el flux i es propaga cap amunt fins que algú l'atrapa o el programa mor; el traceback és el seu informe forense i es llegeix de baix a dalt; els errors de sintaxi són una altra espècie (es corregeixen, no es gestionen); les excepcions integrades formen una jerarquia de classes amb Exception com a mare, i Python prefereix EAFP — intentar i demanar perdó — davant del LBYL de Path.exists(), que deixa condicions de carrera obertes i només cobreix les fallades que vas saber anticipar. Però de moment continuem igual que l'Anna: veiem venir el cop i no sabem encaixar-lo. La propera lliçó introdueix l'eina central de tot el mòdul, try/except, i amb ella saldarem dos deutes històrics: carregar_cataleg() deixarà de dependre que cataleg.json existeixi, i l'int(input()) que arrossega el seu ValueError des del mòdul 1 tindrà per fi una validació digna.

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