Fins ara les excepcions ens arribaven: les llançava float(), open(), json.load(). Però el mòdul 5 amagava un avançament que va passar gairebé desapercebut: el __post_init__ de Llibre i les properties de Producte ja feien raise ValueError(...) quan algú intentava un preu negatiu. Aleshores va ser un acte de fe; avui es converteix en decisió de disseny. En aquesta lliçó aprendràs a llançar excepcions amb intenció: quan una situació mereix un raise i quan n'hi ha prou amb retornar None, com escriure missatges que estalviïn una sessió de depuració, com rellançar el que no et toca gestionar i com encadenar amb raise ... from e per no perdre el rastre de l'error original. L'exemple central serà la funció més delicada de Papyrus: vendre(), la que toca l'estoc i la caixa.

Contingut

  1. raise: llançar una excepció a propòsit
  2. El missatge importa: errors accionables
  3. Llançar o retornar None? El criteri del contracte
  4. Guard clauses: validar a la porta
  5. vendre(titol, unitats): l'exemple central
  6. Rellançar (raise a seques) i encadenar (raise ... from e)
  7. Revisitant el mòdul 5: properties i __post_init__ amb vocabulari complet

raise: llançar una excepció a propòsit

La sentència raise crea i llança una excepció, exactament igual que ho faria Python pel seu compte:

def aplicar_descompte(preu, percentatge):
    if not 0 <= percentatge <= 1:
        raise ValueError(f"percentatge ha d'estar entre 0 i 1, no {percentatge}")
    return preu * (1 - percentatge)

Anatomia: raise + una instància d'excepció (es construeix com qualsevol objecte: classe i arguments, normalment el missatge). A partir d'aquí, el mecanisme és el de 07-01: l'execució s'interromp, l'excepció es propaga cap amunt, i o algú l'atrapa amb el try/except de 07-02 o el programa acaba amb el seu traceback.

I quin tipus llançar? Mentre no tinguem excepcions pròpies (07-04), es trien les integrades respectant-ne el significat, el mateix de la taula de 07-01:

Situació Tipus adequat
El valor no val, encara que el tipus sigui correcte (percentatge=3) ValueError
El tipus no és l'esperat (unitats="dos") TypeError
S'ha buscat una clau/element que no hi és KeyError / LookupError
L'operació no és vàlida en l'estat actual de l'objecte RuntimeError (o una de pròpia, a 07-04)

Llançar un ValueError quan el problema és de tipus despista qui depuri. El tipus de l'excepció és part del missatge.

El missatge importa: errors accionables

Compara aquests dos llançaments davant del mateix problema:

raise ValueError("error a unitats")                            # ❌ quin error? quin valor ha arribat?
raise ValueError(f"unitats ha de ser un enter positiu, no {unitats!r}")   # ✅

Un bon missatge respon tres preguntes: què s'esperava, què ha arribat i — si no és obvi — què cal fer-hi. El !r a la f-string (equival a aplicar-hi repr()) mostra el valor amb les seves cometes i el seu tipus visible: no '3' delata que ha arribat un string "3" en comptes de l'enter 3, un matís que no 3 amagaria. Recorda 07-01: el missatge del raise és exactament el que apareixerà a l'última línia del traceback, la primera que llegirà qui depuri. Escriu-lo per a aquesta persona — que d'aquí a tres mesos seràs tu.

Llançar o retornar None? El criteri del contracte

Aquesta és la decisió de disseny central de la lliçó, i el preu_soci() de l'exercici 3 de 07-02 la va deixar oberta. La brúixola és la idea de contracte: tota funció promet alguna cosa ("dona'm un títol i unes unitats vàlides i descomptaré estoc"). Aleshores:

  • Llança una excepció quan qui crida trenca el contracte — demana una cosa absurda o impossible: vendre −3 unitats, un preu negatiu, un codi de soci amb format invàlid. Això és un error de programació o d'ús i ha de fer soroll.
  • Retorna None quan l'absència és una resposta legítima a una pregunta ben formulada: "teniu Rayuela?" — no tenir-lo no és cap error, és informació.
Criteri Retornar None Llançar excepció
La situació és un resultat normal del negoci? Sí: "no el tenim" és una resposta No: és una violació del contracte
Qui en té la culpa? Ningú: la pregunta era vàlida Qui crida: ha demanat una cosa invàlida
Què farà qui crida, gairebé sempre? Comprovar i continuar (if llibre is None:) No hauria de passar; si passa, millor soroll que silenci
Risc si s'ignora? Baix: None explota aviat i a prop (AttributeError) Alt: continuar amb estoc negatiu corromp dades
Exemple Papyrus cercar_llibre("Rayuela")None vendre("Hamlet", -3)ValueError

El cas canònic de la columna esquerra és cercar_llibre(), que formalitzem així:

def cercar_llibre(cataleg, titol):
    """Retorna el Llibre corresponent al titol, o None si no hi es.

    Que un llibre no sigui al cataleg NO es cap error: es la resposta
    a la pregunta 'El teniu?'. Per aixo None, i no una excepcio.
    """
    return cataleg.get(titol.strip().casefold())

dict.get() (mòdul 4) retorna None quan la clau no existeix: encaixa com un guant amb aquest contracte. Qui crida ha de comprovar el resultat — if llibre is None: — i decidir. Compte, això sí, amb el None que viatja sense comprovar-se: acabarà en un AttributeError: 'NoneType' object has no attribute 'preu' dues funcions més enllà. Per això a 07-04 crearem una germana estricta, obtenir_llibre(), que llança en comptes de retornar None, i veurem que totes dues poden conviure: cadascuna serveix un tipus de cridador diferent.

Guard clauses: validar a la porta

Una guard clause (clàusula de guarda) és una comprovació al principi de la funció que llança immediatament si un argument no compleix el contracte. És LBYL del bo — el que 07-01 va declarar legítim: validar dades que ja tens a la mà, sense finestra temporal ni món exterior pel mig.

def registrar_soci(nom, codi):
    # --- guardes: la porta d'entrada ---
    if not nom.strip():
        raise ValueError("el nom del soci no pot estar buit")
    if not codi.strip():
        raise ValueError("el codi del soci no pot estar buit")
    # --- a partir d'aqui, terreny segur ---
    ...

Avantatges de validar a la porta i no a mitja funció:

  • Fallar aviat (fail fast): l'excepció salta a la primera línia, amb la dada acabada d'arribar, no vint línies després quan ja és difícil saber d'on ha vingut el valor dolent. Un estoc negatiu detectat en vendre és un misteri; detectat en entrar, és un traceback que assenyala el culpable.
  • El cos respira: superades les guardes, la resta de la funció pot assumir dades vàlides sense if niats.
  • El contracte queda escrit: les guardes documenten, millor que cap comentari, què accepta la funció.

vendre(titol, unitats): l'exemple central

Ajuntem-ho tot a la funció més sensible de Papyrus. Vendre toca l'estoc i la caixa: aquí un error silenciós costa diners, així que el contracte es defensa amb raise:

IVA_LLIBRES = 0.04

def vendre(cataleg, titol, unitats):
    """Descompta estoc i retorna la quantia amb IVA de la venda.

    Llanca ValueError si les unitats no son valides o si la venda
    es impossible (titol desconegut o estoc insuficient).
    """
    # Guardes sobre els arguments: qui crida trenca el contracte
    if not isinstance(unitats, int):
        raise TypeError(f"unitats ha de ser int, no {type(unitats).__name__}")
    if unitats <= 0:
        raise ValueError(f"unitats ha de ser positiu, no {unitats}")

    # Guardes sobre l'estat: la venda demanada es impossible
    llibre = cercar_llibre(cataleg, titol)
    if llibre is None:
        raise ValueError(f"'{titol}' no es al cataleg")
    if llibre.estoc < unitats:
        raise ValueError(
            f"estoc insuficient de '{llibre.titol}': "
            f"en queden {llibre.estoc}, se n'han demanat {unitats}"
        )

    # Terreny segur: executar la venda
    llibre.estoc -= unitats
    return round(llibre.preu * unitats * (1 + IVA_LLIBRES), 2)

Decisions de disseny, una a una:

  • unitats <= 0 és ValueError, no un return None: demanar −3 exemplars no és "una resposta legítima del negoci", és un cridador equivocat (o un bug aigües amunt). Soroll, no silenci.
  • vendre() fa servir cercar_llibre() per dins i tradueix el seu None a excepció. És un patró elegant: la funció tolerant respon preguntes; la funció que executa accions converteix l'absència en violació de contracte, perquè vendre el que no existeix sí que és un error.
  • El missatge d'estoc porta els tres números que importen: quin llibre, quant en queda, quant se n'ha demanat. A 07-04 aquestes tres dades ascendiran a atributs d'una excepció pròpia (EstocInsuficientError), perquè el codi — no només l'humà — les pugui llegir.
  • La mutació va al final. Totes les guardes s'avaluen abans de tocar llibre.estoc. Si alguna cosa falla, el catàleg queda intacte: res de vendes a mitges.

Amb dades canòniques: vendre(cataleg, "Hamlet", 2) → els 6 d'estoc passen a 4 i retorna 9.95 * 2 * 1.04 = 20.70. vendre(cataleg, "Hamlet", 99)ValueError: estoc insuficient de 'Hamlet': en queden 6, se n'han demanat 99 (res, ni el 6, no ha canviat).

Rellançar (raise a seques) i encadenar (raise ... from e)

De vegades atrapes una excepció però descobreixes que no et toca gestionar-la del tot. Dues eines:

raise a seques dins d'un except rellança la mateixa excepció, amb el seu traceback original intacte. Útil per reaccionar de passada (avisar, registrar, netejar) sense apropiar-te de l'error:

try:
    quantia = vendre(cataleg, titol, unitats)
except ValueError:
    print("Venda fallida; el cataleg NO s'ha modificat.")
    raise           # que decideixi qui hi ha mes amunt

raise NovaExcepcio(...) from e llança una excepció diferent encadenada a l'original. És l'eina per traduir errors tècnics de baix nivell a l'idioma de qui crida, sense destruir l'evidència:

def carregar_cataleg():
    ruta = BASE / "dades" / "cataleg.json"
    try:
        with open(ruta, encoding="utf-8") as f:
            llista = json.load(f)
    except json.JSONDecodeError as e:
        raise RuntimeError(
            f"cataleg no llegible a {ruta}; restaura una copia de copies/"
        ) from e
    return {l["titol"].strip().casefold(): Llibre(**l) for l in llista}

El traceback resultant mostra les dues excepcions, unides per la línia The above exception was the direct cause of the following exception: — a dalt el JSONDecodeError amb la línia exacta del JSON trencat, a sota el RuntimeError amb el consell de restaurar la còpia. Qui crida gestiona un error d'alt nivell; qui depura conserva la causa arrel. (Nota per comparar: a 07-02 aquest cas el vam resoldre amb SystemExit(1), decisió de programa-terminal; from e és la versió de biblioteca, que informa sense decidir per qui crida. A 07-04, RuntimeError cedirà el lloc a una excepció pròpia.)

I què passa si dins d'un except llances sense from? Python encadena igualment, però amb el rètol During handling of the above exception, another exception occurred: — que suggereix un accident dins del gestor, no una traducció deliberada. from e declara la intenció; fes-lo servir sempre que tradueixis.

Eina Què fa Quan fer-la servir
raise Tipus("msg") Llança una excepció nova Violació de contracte detectada per tu (guardes)
raise a seques (en except) Rellança l'actual, traceback intacte Reaccionar de passada sense apropiar-te de l'error
raise Tipus("msg") from e En llança una altra, encadenant la causa Traduir un error tècnic a l'idioma de qui crida

Revisitant el mòdul 5: properties i __post_init__ amb vocabulari complet

Tanquem el cercle. Això vam escriure al mòdul 5, aleshores com a recepta:

@dataclass
class Llibre:
    titol: str
    preu: float
    estoc: int = 0

    def __post_init__(self):
        if self.preu < 0:
            raise ValueError(f"el preu no pot ser negatiu: {self.preu}")
        if self.estoc < 0:
            raise ValueError(f"l'estoc no pot ser negatiu: {self.estoc}")

Amb el vocabulari d'avui, cada peça té nom: __post_init__ és una guard clause del constructor — valida a la porta, falla aviat, i garanteix l'invariant que no pot existir un Llibre amb preu negatiu a tot Papyrus. Llança ValueError perquè el tipus és correcte i el valor no. El setter de la property preu de Producte fa el mateix a cada assignació posterior: el contracte no se signa només en néixer, es defensa tota la vida de l'objecte. I això explica una cosa de 07-02: quan carregar_cataleg() reconstrueix amb Llibre(**dades), un JSON amb "preu": -5 dispara aquest ValueError automàticament — la validació del mòdul 5 protegeix també la frontera de la persistència del mòdul 6, gratis. Els sistemes ben dissenyats encaixen així.

Errors Comuns i Consells

  • Retornar codis d'error (-1, False, "") en comptes de llançar. L'error viatja disfressat de dada i explota lluny, il·legible. En Python, contracte trencat = excepció; absència legítima = None documentat.
  • Llançar per a tot, fins i tot per a absències normals. L'extrem oposat: si cercar_llibre() llancés per cada títol no trobat, consultar el catàleg s'ompliria de try/except trivials. Aplica la taula del contracte, no un reflex.
  • Atrapar la teva pròpia excepció dues línies després. Si llances i atrapes a la mateixa funció, gairebé segur que el raise sobrava: era un simple if. raise és per creuar fronteres de responsabilitat.
  • Perdre la causa original en traduir. except JSONDecodeError: raise RuntimeError("cataleg no llegible") sense from e deixa la traducció amb aspecte d'accident. Amb from e la cadena queda explícita i completa.
  • Missatges sense la dada culpable. raise ValueError("unitats invalides") obliga a depurar; raise ValueError(f"unitats ha de ser positiu, no {unitats!r}") s'arregla d'un cop d'ull.
  • Consell: escriu primer la docstring amb el contracte ("llança X si..."), després les guardes que el fan complir. Si no saps què prometre, encara no saps què llançar.
  • Consell: muta l'estat sempre després de totes les guardes. Una funció que valida a mitges i modifica a mitges deixa dades corruptes, que són pitjors que qualsevol excepció.

Exercicis

  1. reposar(cataleg, titol, unitats). Escriu la funció que suma estoc a un llibre existent. Contracte: TypeError si unitats no és int; ValueError si unitats <= 0; ValueError si el títol no és al catàleg (l'Anna no reposa el que no ha donat d'alta). Retorna l'estoc resultant. Comprova-la reposant 5 exemplars de Faust (que M6 va deixar en 5): ha de retornar 10 i els casos invàlids han de llançar amb missatges que incloguin la dada culpable.
  2. None o raise? Decideix i justifica en una frase, fent servir la taula del contracte: (a) cercar_soci(codi) quan "LLUIS-001" no és a socis.json; (b) la mateixa funció quan rep codi=""; (c) preu_amb_iva(preu) quan preu és negatiu; (d) darrera_venda() quan vendes.csv és buit després d'un dia sense vendes.
  3. Traduir amb from. Escriu carregar_socis() que llegeixi dades/socis.json i, si el JSON és corrupte, llanci RuntimeError("fitxer de socis no llegible; restaura una copia") encadenat a l'error original amb from. Si el fitxer no existeix, ha de retornar [] (per què aquesta asimetria entre les dues fallades?).

Solucions

  1. def reposar(cataleg, titol, unitats):
        """Suma unitats a l'estoc d'un llibre ja catalogat.
    
        Llanca TypeError/ValueError si es viola el contracte.
        Retorna l'estoc resultant.
        """
        if not isinstance(unitats, int):
            raise TypeError(f"unitats ha de ser int, no {type(unitats).__name__}")
        if unitats <= 0:
            raise ValueError(f"unitats ha de ser positiu, no {unitats}")
        llibre = cercar_llibre(cataleg, titol)
        if llibre is None:
            raise ValueError(f"'{titol}' no es al cataleg; dona'l d'alta primer")
        llibre.estoc += unitats
        return llibre.estoc
    

    reposar(cataleg, "Faust", 5) → 10. L'estructura calca la de vendre(): guardes d'arguments, guarda d'estat, mutació al final. Quan dues funcions comparteixen contracte tan de prop, estan demanant una jerarquia d'excepcions comuna — 07-04.

  2. (a) None: preguntar per un soci que no existeix és una consulta legítima; l'absència és informació ("no és soci → sense descompte"). (b) raise ValueError: un codi buit no és una pregunta vàlida sinó un cridador trencat — guard clause. La mateixa funció pot així retornar None i llançar: responen a situacions diferents. (c) raise ValueError: un preu negatiu viola el contracte; retornar None deixaria passar el bug en silenci. (d) None (o [] si retorna una llista): un dia sense vendes és un resultat normal del negoci, no un error — que el tancament de caixa mostri 0.00 €, no un traceback.

  3. def carregar_socis():
        ruta = BASE / "dades" / "socis.json"
        try:
            with open(ruta, encoding="utf-8") as f:
                return json.load(f)
        except FileNotFoundError:
            return []
        except json.JSONDecodeError as e:
            raise RuntimeError("fitxer de socis no llegible; restaura una copia") from e
    

    L'asimetria és la de 07-02, ara amb nom de contracte: fitxer absent és un estat legítim (encara no hi ha socis: llista buida, com el None de la taula); fitxer corrupte vol dir que hi ha dades però no les podem llegir — retornar [] les donaria per perdudes en silenci i el següent desat les aixafaria. Contracte trencat pel món exterior: excepció, traduïda a l'idioma de Papyrus amb la causa encadenada.

Conclusió

raise ha convertit Papyrus de reactiva en assertiva: les seves funcions ja no només encaixen errors aliens, sinó que defensen contractes propis. Tens el criteri (None per a absències legítimes com cercar_llibre(), excepció per a contractes trencats com vendre() amb unitats negatives), la tècnica (guard clauses que fallen aviat i muten tard, missatges amb la dada culpable en !r) i les eines de propagació (raise a seques per no apropiar-te de l'error, raise ... from e per traduir sense destruir l'evidència). Fins i tot les validacions del mòdul 5 han revelat el seu veritable nom: guardes de constructor que protegeixen invariants. Però fixa't en una esquerda: vendre() llança ValueError tant per "unitats negatives" com per "títol desconegut" com per "estoc insuficient" — i qui atrapi aquest ValueError no pot distingir-los sense llegir el missatge amb els ulls. El codi mereix alguna cosa millor que analitzar strings. La propera lliçó crea la jerarquia d'excepcions pròpia de Papyrus — ErrorPapyrus, LlibreNoTrobatError, EstocInsuficientError — amb atributs que les màquines puguin llegir, no només els humans.

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