El mòdul ha anat deixant avisos pel camí: print("Avis: no existeix cataleg.json..."), print("Fila ignorada..."). Funcionaven a classe, però imagina l'escena real: l'Anna tanca la botiga un dissabte, apaga l'ordinador i el dilluns el tancament de caixa no quadra. Què va passar dissabte? Els print() van morir amb el terminal; no en queda ni rastre. Els avisos per pantalla serveixen per a qui està mirant; per diagnosticar el que va passar quan ningú no mirava cal un registre persistent: el mòdul logging de la biblioteca estàndard. En aquesta última lliçó del mòdul aprendràs els seus nivells, la seva configuració bàsica cap a dades/papyrus.log, el patró del logger per mòdul i la joia logger.exception(), que desa el traceback complet al fitxer. Tancarem amb el decàleg de bones pràctiques de tot el mòdul 7 i amb un caixa.py que ajunta les tres peces: try/except, excepcions pròpies i logging.

Contingut

  1. Per què print() no serveix per diagnosticar
  2. Els cinc nivells: de DEBUG a CRITICAL
  3. logging.basicConfig: format i sortida a fitxer
  4. Un logger per mòdul: getLogger(__name__)
  5. logger.exception(): el traceback, al log
  6. Què registrar i què no
  7. Decàleg del mòdul 7 i el caixa.py robust

Per què print() no serveix per diagnosticar

print() i logging responen preguntes diferents: print() parla amb l'usuari que hi ha al davant; el log parla amb el desenvolupador que arribarà després. Tan bon punt el programa viu més d'una sessió, print() com a eina de diagnòstic falla per tot arreu:

Necessitat en producció print() logging
Que quedi rastre en tancar el terminal No: s'esfuma Sí: fitxer persistent
Saber quan ha passat cada cosa Caldria concatenar la data a mà a cada print Timestamp automàtic al format
Distingir rutina d'emergència Tot s'imprimeix igual Nivells: INFO vs ERROR
Apujar/abaixar el detall sense tocar codi Afegir/esborrar prints per tot el fitxer Canviar level= en un sol lloc
Desar el traceback d'una excepció gestionada Copiar-lo a mà (ningú no ho fa) logger.exception() ho fa tot sol
Saber quin mòdul ha parlat Anònim El nom del logger va a cada línia

Ull al matís: print() no queda prohibit. El menú de la caixa, el "Venda feta: 20.70 €", els missatges a la Júlia i l'Omar — això és interfície d'usuari i continuarà sent print() (fins que el mòdul 10 ho converteixi en web). El que emigra al log és la telemetria: avisos, errors, molles de pa per al diagnòstic.

Els cinc nivells: de DEBUG a CRITICAL

Cada missatge de log porta un nivell de gravetat. Són cinc, ordenats, i el logger té un llindar: només registra el que iguala o supera el seu nivell configurat.

Nivell Quan fer-lo servir (criteri Papyrus) Exemple
DEBUG Detall fi per desenvolupar; en producció sol anar apagat clau normalitzada: "l'odissea"
INFO La vida normal de la botiga: fites que confirmen que tot va bé "Cataleg carregat: 4 llibres" / "Venda: Hamlet x2, 20.70 €"
WARNING Alguna cosa estranya però el programa continua; algú hi hauria de fer un cop d'ull "Fila 7 de vendes.csv no llegible, ignorada"
ERROR Una operació ha fallat; el programa sobreviu però aquella acció no s'ha fet "No s'ha pogut desar el cataleg"
CRITICAL El programa no pot continuar "cataleg.json corrupte i sense copies a copies/"

La prova del cotó per triar nivell: què hauria de fer qui ho llegeixi? Res, només context → DEBUG/INFO. Fer-hi un cop d'ull aquesta setmana → WARNING. Actuar avui → ERROR. Deixar el que estigui fent i córrer → CRITICAL. Amb el llindar a INFO (típic de producció), els DEBUG es descarten sense cost; baixes a DEBUG quan persegueixes una fallada, i el mateix codi es torna loquaç sense afegir ni un sol print.

logging.basicConfig: format i sortida a fitxer

Una sola crida, al principi del programa principal i només una vegada, configura el sistema sencer:

# caixa.py — en arrencar, abans de cap altra cosa
import logging
from pathlib import Path

BASE = Path(__file__).parent          # l'ancora del modul 6
(BASE / "dades").mkdir(exist_ok=True)

logging.basicConfig(
    filename=BASE / "dades" / "papyrus.log",
    level=logging.INFO,
    format="%(asctime)s %(levelname)-8s %(name)s: %(message)s",
    encoding="utf-8",
)

Peça a peça:

  • filename: els missatges van a dades/papyrus.log en comptes de a pantalla. S'obre en mode append (a, del mòdul 6): cada arrencada suma, no aixafa — l'historial és justament el valor.
  • level=logging.INFO: el llindar. DEBUG es descarta; d'INFO cap amunt, s'escriu.
  • format: la plantilla de cada línia. %(asctime)s és la data i l'hora, %(levelname)-8s el nivell alineat a 8 caràcters, %(name)s el logger que parla, %(message)s el teu text. (És la sintaxi clàssica de %, anterior a les f-strings; logging la conserva per història i rendiment.)
  • encoding="utf-8": l'innegociable del mòdul 6 també regeix aquí — al log hi haurà títols amb accents.

El resultat, tal com es veu dins de papyrus.log:

2026-07-13 09:00:12,451 INFO     magatzem: Cataleg carregat: 4 llibres
2026-07-13 09:14:03,102 INFO     magatzem: Venda: Hamlet x2, 20.70 €
2026-07-13 09:31:44,876 WARNING  caixa: Fila ignorada a vendes.csv: {'data': '2026-07-12', 'titol': 'Faust', 'quantia': 'gratis'}

Data, gravetat, mòdul, missatge amb la dada culpable: el dilluns al matí, l'Anna (o tu) reconstrueix el dissabte línia a línia.

Un logger per mòdul: getLogger(__name__)

No es crida logging.info(...) a pèl per tot el codi. El patró professional crea un logger amb nom a la capçalera de cada mòdul:

# magatzem.py
import logging

logger = logging.getLogger(__name__)   # nom: "magatzem"

def carregar_cataleg():
    ...
    logger.info("Cataleg carregat: %d llibres", len(cataleg))
  • __name__ és la variable que el mòdul 3 ens va presentar (la de l'if __name__ == "__main__":): val "magatzem" quan el mòdul s'importa. Així, cada línia del log declara el seu origen — la columna %(name)s del format — sense esforç.
  • Divisió de poders: els mòduls (magatzem.py, errors.py...) emeten amb el seu logger; només el programa principal configura amb basicConfig. Una biblioteca que configurés el logging trepitjaria la configuració de qui la importa.
  • L'estil %d amb arguments separats (logger.info("...: %d llibres", len(cataleg))) deixa que logging compongui el text només si el missatge passa el llindar. Amb f-strings també funciona i és acceptable mentre aprens; l'estil diferit és l'idiomàtic.

logger.exception(): el traceback, al log

La joia de la corona. Dins d'un except, logger.exception(...) registra el missatge a nivell ERROR i hi afegeix el traceback complet de l'excepció en curs — l'informe forense de 07-01, arxivat automàticament:

def desar_cataleg(cataleg):
    ruta = BASE / "dades" / "cataleg.json"
    try:
        with open(ruta, "w", encoding="utf-8") as f:
            json.dump([asdict(l) for l in cataleg.values()], f,
                      ensure_ascii=False, indent=2)
    except OSError:
        logger.exception("No s'ha pogut desar el cataleg a %s", ruta)
        raise    # registrar NO es gestionar: que decideixi qui ha cridat

A papyrus.log apareix la línia ERROR seguida del Traceback (most recent call last): ... íntegre, amb fitxer, línia i causa. Tres regles d'ús:

  • Només dins d'un except — és on existeix "l'excepció en curs" que cal adjuntar.
  • Combina perfectament amb el raise a seques de 07-03: registrar a la frontera i rellançar. El log guarda l'evidència; l'excepció continua el seu viatge cap a qui pugui decidir. Registrar no és gestionar.
  • Substitueix l'antipatró definitiu: except OSError: pass era silenci culpable (07-02); except OSError: logger.exception(...) seguit d'una decisió conscient (rellançar, valor per defecte, avortar) és enginyeria.

Què registrar i què no

Un log útil explica la història operativa del programa. Registra: arrencada i aturada (amb versió i configuració carregada), fites de negoci a INFO (vendes, altes, reposicions, tancaments de caixa), tota anomalia a WARNING+ amb la dada culpable ({fila!r}, la ruta, el títol), i el traceback via logger.exception() a cada except fronterer.

I el que no hi ha d'entrar, perquè el log és un fitxer de text pla que es copia, es comparteix i acaba en correus de suport:

  • Dades personals i sensibles: res de noms complets de clients associats a les seves compres, adreces, telèfons, dades bancàries o de targetes. "Venda: Hamlet x1, soci MARTA-002" val (un codi intern opac); "Marta García, targeta 4779..." mai. La legislació de protecció de dades (a Europa, el RGPD) converteix això en obligació legal, no només en cortesia — i un log no té el control d'accés d'una base de dades.
  • Secrets: contrasenyes, tokens, claus d'API. Ni tan sols "per depurar un moment": els logs s'arxiven i els secrets que s'hi escriuen queden compromesos.
  • Soroll: registrar cada iteració d'un bucle a INFO enterra el senyal. Per a això existeix DEBUG, que en producció va apagat.

Decàleg del mòdul 7 i el caixa.py robust

Les deu regles que resumeixen tot el mòdul, cadascuna amb la seva lliçó d'origen:

  1. Llegeix el traceback de baix a dalt: tipus i missatge primer, cadena de crides després (07-01).
  2. EAFP davant del món exterior: intenta i gestiona; Path.exists() era un pal·liatiu amb condició de carrera (07-01).
  3. Captura tipus concrets: mai un except: nu; Exception només a l'última frontera, i registrant (07-02).
  4. try mínim: només la línia que pot fallar; else per al camí feliç, finally per a la neteja incondicional (07-02).
  5. No silenciïs: un error atrapat es compta, es registra o es rellança; mai pass a seques (07-02, 07-05).
  6. Falla aviat: guard clauses a la porta, mutació de l'estat només després de superar totes les guardes (07-03).
  7. Missatges accionables: què s'esperava, què ha arribat (!r) i, si escau, què cal fer (07-03).
  8. None per a absències legítimes, excepció per a contractes trencats; si conviuen, que sigui amb criteri, com cercar_llibre()/obtenir_llibre() (07-03, 07-04).
  9. Jerarquia pròpia amb atributs: l'except distingeix el teu error de la resta i el codi llegeix dades, no strings (07-04).
  10. Log a la frontera: registra amb logger.exception() on gestiones, no a cada nivell intermedi — un error, una entrada al log, no cinc (07-05).

I el tancament pràctic: caixa.py ajuntant les tres peces del mòdul.

# caixa.py — el programa que l'Anna executa cada mati
import logging
from pathlib import Path

from magatzem import carregar_cataleg, desar_cataleg, vendre
from errors import ErrorPapyrus, LlibreNoTrobatError, EstocInsuficientError

BASE = Path(__file__).parent
(BASE / "dades").mkdir(exist_ok=True)
logging.basicConfig(
    filename=BASE / "dades" / "papyrus.log",
    level=logging.INFO,
    format="%(asctime)s %(levelname)-8s %(name)s: %(message)s",
    encoding="utf-8",
)
logger = logging.getLogger(__name__)


def demanar_enter(missatge):                    # el deute de M1, saldat a 07-02
    while True:
        text = input(missatge)
        try:
            return int(text)
        except ValueError:
            print(f"'{text}' no es un nombre enter. Torna-ho a provar.")


def main():
    logger.info("Papyrus obre la caixa")
    cataleg = carregar_cataleg()                # EAFP per dins (07-02)

    while True:
        titol = input("\nTitol (o 'fi'): ").strip()
        if titol.casefold() == "fi":
            break
        unitats = demanar_enter("Unitats: ")
        try:
            quantia = vendre(cataleg, titol, unitats)
        except LlibreNoTrobatError as e:        # fulla: reaccio especifica (07-04)
            print(f"No tenim '{e.titol}'. Vols que l'encarreguem?")
            logger.warning("Venda fallida, titol desconegut: %r", e.titol)
        except EstocInsuficientError as e:      # fulla: atributs llegibles (07-04)
            print(f"Nomes en queden {e.disponible} de '{e.titol}'.")
            logger.warning("Estoc curt: %s", e)
        except ErrorPapyrus as e:               # base: xarxa del negoci (07-04)
            print(f"Operacio no feta: {e}")
            logger.error("Error de negoci no especific: %s", e)
        else:                                   # cami felic fora del try (07-02)
            print(f"Venda feta: {quantia:.2f} € (amb IVA)")
            logger.info("Venda: %s x%d, %.2f €", titol, unitats, quantia)

    desar_cataleg(cataleg)
    logger.info("Papyrus tanca la caixa")


if __name__ == "__main__":
    try:
        main()
    except Exception:                           # UNICA frontera amb Exception (regla 3)
        logger.exception("Error no previst: la caixa es tanca d'emergencia")
        print("Hi ha hagut un error inesperat. Consulta dades/papyrus.log")
        raise SystemExit(1)

Fixa't en l'arquitectura de tres anells: les fulles de la jerarquia es gestionen a tocar del taulell (reacció específica, WARNING), la base ErrorPapyrus fa de xarxa del negoci, i un únic except Exception a la porta del programa — amb logger.exception() i un missatge honest a pantalla — caça l'imprevist sense silenciar-lo. print() per a l'Anna, logging per al diagnòstic: cada missatge, al seu públic.

Queda una menció honrada: en sistemes que corren setmanes, papyrus.log creix sense límit. El mateix logging porta la solució — handlers com RotatingFileHandler, que parteix el log per mida o per data, i configuració des de fitxer — però és tema avançat que excedeix l'essencial; en tindràs prou sabent que existeix i on buscar-lo (logging.handlers) quan la botiga ho necessiti.

Errors Comuns i Consells

  • Cridar basicConfig() a cada mòdul. Només el programa principal configura; els mòduls creen el seu getLogger(__name__) i emeten. A més, basicConfig només té efecte la primera vegada: les crides repetides s'ignoren en silenci i confonen.
  • logger.error(e) dins de l'except en comptes de logger.exception(...). Desa el missatge però llença el traceback, que era la meitat valuosa. Dins d'except, gairebé sempre exception().
  • Registrar i creure que ja està gestionat. El log és memòria, no decisió: després de registrar, decideix — rellança, aplica un valor per defecte o avorta. Registrar-i-empassar és l'antipatró pass amb millor vestuari.
  • Registrar el mateix error a cada nivell de la pila. Cinc entrades idèntiques per una sola fallada embruten el diagnòstic. Regla 10: es registra a la frontera on es gestiona.
  • Dades sensibles al log. Revisa cada logger.* com si el fitxer l'hagués de llegir un desconegut — perquè tard o d'hora ho farà. Codis interns sí; identitats, targetes i secrets, mai.
  • Consell: durant el desenvolupament, canvia el llindar a level=logging.DEBUG i afegeix DEBUG generosos a la lògica delicada (normalització de claus, conversions). En producció, llindar a INFO: els DEBUG continuen allà, adormits i gratis.
  • Consell: obre papyrus.log de tant en tant encara que res no falli. Un log que només es llegeix a les emergències sol descobrir-se buit, mal formatat o mut just quan més falta fa.

Exercicis

  1. tancament_de_caixa() amb telemetria. Adapta la versió de 07-02 perquè faci servir un logger de mòdul: cada fila corrupta a WARNING amb la fila completa (%r), el total del dia a INFO, i davant de FileNotFoundError un INFO ("sense vendes avui") — per què INFO i no ERROR? Els print() de resum per a l'Anna es mantenen: raona quin missatge pertany a cada canal.

  2. La còpia nocturna que avisa. Embolica la copia_de_seguretat() del mòdul 6 (que copia cataleg.json a copies/cataleg-<data>.json amb shutil.copy2) perquè: registri a INFO la destinació si tot va bé; i davant d'OSError faci servir logger.exception() i rellanci. Explica per què rellançar en comptes d'empassar-se l'error, sent "només" una còpia.

  3. Auditoria del decàleg. Aquest fragment infringeix com a mínim quatre regles del decàleg. Identifica-les pel número i reescriu-lo:

    def alta_soci(socis, nom, codi):
        try:
            logging.basicConfig(level=logging.DEBUG)
            if codi in socis:
                return False
            socis.append(codi)
            logging.info("Alta de soci: " + nom + " amb targeta 4779-1234-5678-9010")
            return True
        except:
            pass
    

Solucions

  1. logger = logging.getLogger(__name__)
    
    def tancament_de_caixa():
        ruta = BASE / "dades" / "vendes.csv"
        total, vendes_ok, 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"])
                    except ValueError:
                        corruptes += 1
                        logger.warning("Fila no llegible a vendes.csv: %r", fila)
                    else:
                        vendes_ok += 1
        except FileNotFoundError:
            logger.info("Tancament sense vendes: no existeix %s", ruta)
            print("Avui no hi ha vendes registrades.")
            return 0.0
        logger.info("Tancament de caixa: %d vendes, %d corruptes, total %.2f €",
                    vendes_ok, corruptes, total)
        print(f"Tancament de caixa: {vendes_ok} vendes, total {total:.2f} €")
        return total
    

    INFO i no ERROR perquè un dia sense vendes és un resultat legítim del negoci (el criteri de 07-03 aplicat a nivells de log): ningú no ha d'"actuar avui". Canals: el print dona a l'Anna el resum que necessita en el moment; el log guarda a més les files corruptes exactes i el desglossament, que és el que el diagnòstic del dilluns necessitarà.

  2. def copia_de_seguretat():
        origen = BASE / "dades" / "cataleg.json"
        destinacio = BASE / "copies" / f"cataleg-{date.today().isoformat()}.json"
        try:
            shutil.copy2(origen, destinacio)
        except OSError:
            logger.exception("Copia de seguretat fallida cap a %s", destinacio)
            raise
        logger.info("Copia de seguretat creada: %s", destinacio)
    

    Es rellança perquè una còpia fallida silenciada és una falsa sensació de seguretat: el dia que l'Anna necessiti restaurar des de copies/, descobriria que fa setmanes que no es creen. Regla 5 (no silenciar) i el patró registrar-i-rellançar de 07-03/07-05: el log conserva el traceback i qui ha cridat decideix si avortar el tancament o avisar a crits.

  3. Infraccions: regla 3 (except: nu), regla 5 (pass: l'error desapareix, i a sobre la funció retorna None en comptes de True/False, un tercer estat fantasma), regla 10 / patró de configuració (basicConfig dins d'una funció de biblioteca, trepitjant — o sent ignorada per — la configuració del programa principal), i la norma de dades sensibles (un número de targeta escrit al log). De propina: logger anònim en comptes de getLogger(__name__) i un try que abriga codi que no pot fallar de manera esperable (regla 4). Reescriptura:

    logger = logging.getLogger(__name__)
    
    def alta_soci(socis, nom, codi):
        if not nom.strip() or not codi.strip():
            raise ValueError("nom i codi no poden estar buits")   # regla 6
        if codi in socis:
            logger.info("Alta rebutjada, codi duplicat: %r", codi)
            return False
        socis.append(codi)
        logger.info("Alta de soci: %s", codi)    # codi intern; ni nom ni targeta
        return True
    

Conclusió

El mòdul 7 respon, una a una, les preguntes que va deixar obertes el tancament del mòdul 6. I si el fitxer no hi és? carregar_cataleg() ho intenta a l'estil EAFP i converteix el FileNotFoundError en un catàleg buit amb avís — sense el pal·liatiu Path.exists() ni la seva condició de carrera (07-01, 07-02). I si la còpia és corrupta? JSONDecodeError es distingeix del fitxer absent i es tradueix amb raise ... from e sense perdre l'evidència (07-02, 07-03). I si float(fila["preu"]) rep "gratis"? Un try mínim salta la fila, la compta i la deixa escrita a WARNING a papyrus.log (07-02, 07-05). I si la Júlia tecleja un preu impossible? Les guard clauses i el __post_init__ de M5 — que ara saps llegir com a contractes — el rebutgen amb un missatge accionable, i demanar_enter() va saldar de passada el deute que int(input()) arrossegava des del mòdul 1 (07-03, 07-02). Pel camí, Papyrus ha guanyat un vocabulari propi d'errors — ErrorPapyrus i les seves fulles amb atributs, a errors.py — i una caixa de tres anells que atrapa l'específic a tocar del taulell, el genèric a la frontera i ho deixa tot anotat amb logger.exception() (07-04, 07-05). La botiga ja no cau: s'explica. El mòdul 8 canvia de terç: el codi de Papyrus és correcte i robust, i ara toca fer-lo més expressiu i més eficient — type hints que documenten els contractes que aquest mòdul defensava a mà, decoradors que emboliquen funcions sense repetir codi, generadors que processen vendes sense carregar-les senceres a memòria, i el moment de saldar la promesa pendent del with: entendre els context managers per dins i, per fi, construir els teus.

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