Amb la v0.18 el paquet tascafacil/ s'entén: té docstrings, anotacions de tipus i un README. Però continua sent fràgil. Si la Marta obre el tasques.json amb un editor i s'hi deixa una coma de més, el programa mor amb un traceback abans de mostrar el menú. Si en Luis escriu «tres» quan se li demanen els dies, mor igualment. I tot el que hi hagués a la memòria es perd. Aquesta lliçó tanca per fi la promesa que el curs arrossega des de Conversió de tipus i validació i des de Desar dades en fitxers: capturar els errors amb try/except en lloc de pregar perquè no passin. Aprendràs a distingir els tres tipus d'error, a llegir un traceback sense por, a decidir què es captura i què es deixa pujar, a llançar excepcions pròpies com TascaInvalida, a registrar el que passa amb logging en lloc de fer-ho amb print i a depurar amb punts d'interrupció en comptes d'endevinar. Al final, TascaFàcil deixarà de trencar-se.

Contingut

  1. Els tres tipus d'error
  2. Llegir un traceback
  3. Catàleg d'excepcions habituals
  4. try/except: capturar el que s'ha de capturar
  5. else i finally: el flux complet
  6. raise i excepcions pròpies
  7. La política d'errors: EAFP davant de LBYL
  8. logging davant de print
  9. Depurar de debò: mètode, VS Code i pdb
  10. TascaFàcil v0.19: el programa que no es trenca
  11. Errors habituals i consells
  12. Exercicis
  13. Conclusió

  1. Els tres tipus d'error

No tots els errors són iguals, i cadascun es caça d'una manera diferent; distingir-los és el primer pas per no perdre el temps buscant on no hi ha res. El tercer és el perillós: els dos primers avisen a crits, però l'error de lògica es queda callat, imprimeix «progrés: 0,6 %» i ningú no se n'adona fins que la Marta pregunta per què la tasca del cartell porta tres setmanes al 0,6 %. Contra aquest no hi ha try/except que valgui: calen el mètode de depuració de l'apartat 9 i les proves de Proves automatitzades.

Tipus Quan apareix Qui el detecta Com s'arregla
De sintaxi Abans d'executar res Python, en llegir el fitxer Corregint l'escriptura
D'execució (excepció) Durant l'execució L'intèrpret, en arribar a la línia Amb validació o try/except
De lògica Mai: el programa «funciona» Només tu, mirant el resultat Depurant i provant
print("Tasques de la Marta"      # 1. SyntaxError: no s executa RES del fitxer
dies = int("tres")               # 2. ValueError: la sintaxi val, la dada no
def progres(fets, dies):         # 3. Error de logica: ningu no protesta...
    return fets / dies           #    ...i hauria de ser fets / dies * 100

  1. Llegir un traceback

Quan salta una excepció, Python imprimeix un traceback: el mapa de com s'ha arribat fins a l'error. Es llegeix d'una manera molt concreta: de baix a dalt per saber què ha passat, i de dalt a baix per saber per què.

Traceback (most recent call last):
  File "/home/marta/projectes/tascafacil/__main__.py", line 34, in main
    agenda = magatzem.carregar()
  File "/home/marta/projectes/tascafacil/magatzem.py", line 41, in carregar
    dades = json.load(f)
  File "/usr/lib/python3.12/json/__init__.py", line 293, in load
    return loads(f.read(), ...)
json.decoder.JSONDecodeError: Expecting ',' delimiter: line 8 column 3

Aquest bloc conté tota la informació que necessites:

  • most recent call last: les crides estan en ordre —a dalt la primera, a baix la que ha esclatat—; això és la pila de crides. I l'última línia diu què ha passat: el tipus (JSONDecodeError) i el missatge (falta una coma a la línia 8 del fitxer de dades).
  • La línia culpable del teu codi és l'última que pertany al teu projecte, aquí magatzem.py, line 41. El que hi ha a sota és la biblioteca estàndard: json no falla, només és on es manifesta.
  • L'origen real sol ser més amunt: carregar() funciona bé; el problema és el fitxer que se li ha donat. Recorre la pila cap amunt preguntant-te «qui li ha passat aquesta dada?»; en el 90 % dels casos l'error hi és.

  1. Catàleg d'excepcions habituals

Aquestes són les que et trobaràs una vegada i una altra; reconèixer-les d'un cop d'ull estalvia moltíssim temps:

Excepció Causa típica
ValueError El tipus és correcte però el valor no: int("tres")
TypeError Operació entre tipus incompatibles: "5" + 5
KeyError Clau que no existeix en un diccionari: tasca["data"]
IndexError Índex fora de rang: tasques[10] amb 3 tasques
FileNotFoundError El fitxer no existeix o la ruta és incorrecta
ZeroDivisionError Divisió o mòdul per zero
AttributeError L'objecte no té aquest atribut o mètode: None.titol
json.JSONDecodeError El fitxer JSON està corrupte o buit

  1. try/except: capturar el que s'ha de capturar

La sintaxi és simple: al bloc try hi va el codi que pot fallar; a l'except, què fer si falla.

entrada = input("Dies estimats: ")
try:
    dies = int(entrada)                      # aquesta linia pot llancar ValueError
except ValueError:
    print("Aixo no es un numero enter. Faig servir 1 dia per defecte.")
    dies = 1                                 # i el programa continua

Sense el try, un «tres» tombava el programa. Amb ell, l'usuari rep un missatge comprensible i l'aplicació segueix viva. Ara bé, hi ha dues maneres d'escriure-ho malament:

except:                    # MALAMENT: captura tot, fins i tot el Ctrl+C de l usuari
    dies = 1
except Exception:          # GAIREBE IGUAL DE MALAMENT: amaga errors que no esperaves
    dies = 1

except: a seques captura fins i tot un KeyboardInterrupt (el Ctrl+C amb què l'usuari intenta sortir) i un SystemExit, de manera que el teu programa esdevé impossible d'interrompre. except Exception no arriba tan lluny, però continua tapant errors de programació —un nom mal escrit, un AttributeError— i converteix una fallada evident en un comportament estrany impossible de diagnosticar. La regla és: captura l'excepció concreta que saps que pot passar i que saps com gestionar; la resta ha de pujar i fer soroll. Quan hi ha diverses possibilitats s'encadenen except o s'agrupen en una tupla, i amb as es recull l'objecte per llegir-ne el missatge:

try:
    with open(ruta, encoding="utf-8") as f:
        agenda = Agenda.from_dict(json.load(f))
except FileNotFoundError:
    agenda = Agenda()                                  # primera execucio
except (json.JSONDecodeError, KeyError) as error:      # dos casos, un bloc
    print(f"El fitxer esta malmes ({error}). Es fara servir una agenda buida.")
    agenda = Agenda()

Els except es comproven en ordre i només s'executa el primer que encaixa: per això les excepcions concretes van sempre abans que les generals.

  1. else i finally: el flux complet

El bloc try admet dues clàusules més que arrodoneixen l'estructura. else s'executa només si no hi ha hagut excepció, cosa que permet deixar al try únicament la línia perillosa; finally s'executa sempre, hagi fallat o no, fins i tot si el try fa return, i és el lloc de la neteja.

def carregar_json(ruta):
    """Retorna les dades del fitxer, o None si no s ha pogut llegir."""
    try:
        with open(ruta, encoding="utf-8") as f:
            dades = json.load(f)
    except (FileNotFoundError, json.JSONDecodeError) as error:
        print(f"No s ha pogut llegir {ruta}: {error}")
        return None
    else:
        return dades                        # nomes si tot ha anat be
    finally:
        print("Fi de l intent de carrega.")  # passi el que passi
flowchart TD
    A[Entra al try] --> B{Hi ha excepcio?}
    B -- No --> C[Executa else] --> G[Executa finally]
    B -- Si --> D{Coincideix algun except?}
    D -- Si --> E[Executa aquest except] --> G
    D -- No --> F[L excepcio puja a qui ha cridat] --> G
    G --> H[Continua el programa o propaga l error]

Fixa't en la branca de la dreta: si cap except no encaixa, el finally s'executa igualment i després l'excepció continua pujant. Aquesta és la clau del disseny: finally garanteix la neteja sense empassar-se l'error. Per als fitxers, això ja ho resol una cosa que fas servir des de 05-05: with tanca el fitxer automàticament i fa innecessari el finally en aquest cas.

  1. raise i excepcions pròpies

Fins ara les excepcions venien de Python. També pots llançar-les tu, i és el correcte quan la teva funció rep alguna cosa amb què no pot treballar: fallar aviat i amb un missatge clar és millor que retornar un valor inventat.

def crear_tasca(titol, dies):
    """Crea una tasca validant les dades d entrada."""
    if not titol.strip():
        raise ValueError("El titol no pot estar buit.")
    if dies <= 0:
        raise ValueError(f"Els dies han de ser positius, i han arribat {dies}.")
    return Tasca(titol, dies)

try:
    tasca = crear_tasca("", 3)
except ValueError:
    print("Avis: dades invalides.")
    raise                      # 'raise' a seques rellanca la mateixa excepcio

Un raise a seques dins d'un except rellança l'excepció original amb el seu traceback intacte, que és el que vols quan només necessites anotar alguna cosa de passada. I quan converteixes un error de baix nivell en un altre de més significatiu, encadena'ls amb raise ... from: raise TascaInvalida("prioritat desconeguda") from error conserva la causa al traceback. Definir una excepció pròpia és sorprenentment simple, una classe buida que hereta d'una altra excepció:

class TascaInvalida(ValueError):
    """Les dades d una tasca no compleixen les regles de l Estudi Alba."""

class PrioritatInvalida(TascaInvalida):     # es poden encadenar en jerarquia
    """La prioritat no es 'alta', 'mitjana' ni 'baixa'."""

Per què heretar de ValueError i no d'Exception? Perquè així el codi antic continua funcionant: qui capturava ValueError per validar entrades continuarà capturant també les teves. Recorda a més que totes les excepcions descendeixen d'Exception, i que aquesta jerarquia mana: except ValueError no atrapa un KeyError, però except Exception els atrapa tots, inclosos els que no esperaves. Una jerarquia pròpia dona precisió en capturar (except TascaInvalida atrapa qualsevol problema de dades de tasca, inclosa PrioritatInvalida, sense tocar un ValueError d'una altra banda), missatges del domini en lloc de missatges sobre conversions de text, i un únic lloc que documenta les regles del negoci. La convenció és agrupar-les al mòdul del domini —a TascaFàcil, model.py— amb noms recognoscibles (...Invalida, ...Error).

  1. La política d'errors: EAFP davant de LBYL

Tota aplicació necessita una política: què es captura i què es deixa pujar. La regla és fàcil de recordar: captura un error només si hi pots fer alguna cosa útil —demanar la dada un altre cop, fer servir un valor per defecte, avisar l'usuari—. Si no pots, deixa'l pujar: una fallada visible és infinitament millor que una de silenciosa. Per validar hi ha a més dos estils, i Python en prefereix un:

if ruta.exists():                  # LBYL (Look Before You Leap), com a 05-05
    with open(ruta, encoding="utf-8") as f:
        dades = json.load(f)
else:
    dades = []

try:                               # EAFP (Easier to Ask Forgiveness than Permission)
    with open(ruta, encoding="utf-8") as f:
        dades = json.load(f)
except FileNotFoundError:          # l estil que prefereix Python
    dades = []
Estil Avantatge Inconvenient
LBYL Es llegeix com una condició normal Deixa una escletxa: el fitxer es pot esborrar entre la comprovació i l'obertura
EAFP No hi ha escletxa: o s'obre o es captura Requereix conèixer l'excepció concreta

Python prefereix EAFP, i amb fitxers és objectivament més segur: entre l'exists() i l'open() passa un instant en què un altre programa pot esborrar o moure el fitxer. Aquella comprovació amb Path.exists() de 05-05 era correcta per al que sabíem llavors; aquesta és la versió robusta que vam prometre.

  1. logging davant de print

Quan alguna cosa falla, la temptació és sembrar el codi de print("he arribat aqui"). Funciona per sortir del pas, però en un programa real és un mal sistema: després s'han d'esborrar (i sempre se n'oblida algun), es barregen amb la sortida legítima i no deixen constància de res. El mòdul logging de la biblioteca estàndard resol les tres coses.

Nivell Quan fer-lo servir Exemple a TascaFàcil
DEBUG Detall intern útil en programar «Carregades 12 tasques de tasques.json»
INFO Esdeveniments normals «Tasca creada: Cartell fira del llibre»
WARNING Alguna cosa estranya, però es pot continuar «El fitxer no existeix; agenda buida»
ERROR / CRITICAL Una operació ha fallat / no es pot continuar «No s'ha pogut desar el JSON»
import logging

logging.basicConfig(
    filename="tascafacil.log",              # sense aixo, surt per pantalla
    level=logging.INFO,                     # es registren INFO i superiors
    format="%(asctime)s %(levelname)s %(message)s",
)
try:
    desar(agenda)
except OSError:
    logging.exception("Error en desar")     # inclou el traceback complet

Tres detalls marquen la diferència. level decideix què es registra: pujant-lo a WARNING silencies els missatges de detall sense tocar ni una línia de codi. filename envia tot a un fitxer, així que demà pots veure què va passar sense haver estat al davant. I logging.exception(), fet servir dins d'un except, escriu el missatge i el traceback sencer: és la manera correcta de registrar una fallada que has capturat i no vols perdre.

  1. Depurar de debò: mètode, VS Code i pdb

Depurar no és mirar el codi fixament fins que confessi. És un mètode de quatre passos:

  1. Reproduir: trobar la seqüència exacta que provoca la fallada, sempre. Un error que no saps reproduir no el pots arreglar.
  2. Aïllar: reduir el cas al mínim. Falla amb una tasca o en calen vint? Amb qualsevol prioritat?
  3. Formular una hipòtesi concreta («crec que dies arriba com a text des del JSON») i comprovar-la: una sola cosa cada vegada; si és falsa, es descarta i se'n formula una altra.

Per a aquest últim pas hi ha el depurador. A VS Code fas clic a l'esquerra del número de línia per posar un punt d'interrupció (un cercle vermell), engegues amb F5 i el programa s'atura just abans d'executar aquella línia; allà pots inspeccionar les variables al panell lateral, veure la pila de crides i avançar amb F10 (executa la línia sencera), F11 (entra a la funció) i F5 (continua). Sense editor gràfic, escriu breakpoint() a la línia que t'interessi i executa: s'obrirà una consola (Pdb) en aquell punt.

Comanda Què fa
l (list) Mostra el codi al voltant de la línia actual
n (next) Executa la línia i passa a la següent
s (step) Entra dins de la funció que es crida
c (continue) Continua fins al breakpoint() següent
p expressio Imprimeix el valor d'una variable o expressió
w (where) / q (quit) Mostra la pila de crides / surt del depurador

I una última eina, en dues línies: assert condicio, "missatge" llança un AssertionError si la condició és falsa. Serveix per comprovar suposicions internes mentre desenvolupes, però no per validar dades de l'usuari, perquè Python pot desactivar els assert amb l'opció -O. El seu lloc natural són les proves, i allà ho reprenem a 08-04.

  1. TascaFàcil v0.19: el programa que no es trenca

Primer, magatzem.py sobreviu que el JSON no existeixi o estigui corrupte:

"""Persistencia de TascaFacil: JSON i exportacio a CSV."""
RUTA_JSON = Path("tasques.json")

def carregar_tasques(ruta: Path = RUTA_JSON) -> Agenda:
    """Retorna l agenda desada; una agenda buida si no s ha pogut llegir."""
    try:
        with open(ruta, encoding="utf-8") as f:
            return Agenda.from_dict(json.load(f))
    except FileNotFoundError:
        logging.info("No existeix %s; es comenca amb una agenda buida.", ruta)
    except json.JSONDecodeError as error:
        logging.error("El fitxer %s esta malmes: %s", ruta, error)
        ruta.replace(ruta.with_suffix(".json.bak"))   # no es perd: s aparta
    return Agenda()

Fixa't en la decisió de disseny: davant d'un fitxer corrupte no s'esborra res, s'aparta amb un altre nom i es continua, de manera que l'usuari no perd les seves dades. Segon, el model valida i llança la seva pròpia excepció, i la interfície la captura per tornar a preguntar:

class Tasca:
    def __init__(self, titol: str, responsable: str, prioritat: str = "mitjana",
                 dies: int = 1) -> None:
        if prioritat.strip().lower() not in PRIORITATS:
            raise TascaInvalida(f"Prioritat no valida: {prioritat!r}")
        try:
            self.dies = int(dies)
        except (ValueError, TypeError) as error:
            raise TascaInvalida(f"Dies no valid: {dies!r}") from error

def demanar_prioritat() -> str:               # a tascafacil/interficie.py
    """Demana una prioritat fins que sigui valida."""
    while True:
        try:
            return Tasca("temporal", "marta", input("Prioritat: ")).prioritat
        except TascaInvalida as error:
            print(f"  {error} Torna-ho a provar.")

I a __main__.py, la configuració del registre i una xarxa de seguretat final perquè cap fallada inesperada no s'endugui per davant les dades:

def main() -> None:
    """Executa TascaFacil registrant els esdeveniments a tascafacil.log."""
    logging.basicConfig(filename="tascafacil.log", level=logging.INFO,
                        format="%(asctime)s %(levelname)s %(message)s")
    agenda = magatzem.carregar_tasques()
    try:
        bucle_principal(agenda)
    except KeyboardInterrupt:                 # l usuari ha premut Ctrl+C
        print("\nSortint...")
    finally:
        magatzem.desar(agenda)                # passi el que passi, es desa

TascaFàcil v0.19: un JSON corrupte ja no tomba el programa, una prioritat inventada es rebutja amb un missatge del domini, un dies de text es converteix o es rebutja amb claredat, un Ctrl+C desa abans de sortir i tot queda registrat a tascafacil.log.

Errors Habituals i Consells

  • Fer servir except: o except Exception per comoditat, o capturar i no fer res (except ValueError: pass). Amaguen errors de programació i creen fallades invisibles: captura l'excepció concreta, i si no hi pots fer res útil, deixa-la pujar.
  • Posar l'except general abans que els concrets, o ficar mig programa dins del try. Es comproven en ordre, així que el primer que encaixi guanya; i al try hi ha d'anar només la línia que pot fallar, amb la resta a l'else.
  • Llegir el traceback per sobre, o depurar amb print en un programa real. L'última línia diu què ha passat i l'última línia teva diu on; i per a la resta hi ha logging, que es filtra per nivell, va a un fitxer i no s'ha d'esborrar després.
  • Consell: escriu el missatge pensant en qui l'ha de llegir. «Prioritat no valida: 'urgent'. Fes servir alta, mitjana o baixa» val mil vegades més que «error de validació».

Exercicis

Exercici 1: Llegir un traceback

Explica què ha passat, en quina línia de codi propi hi ha el problema i com ho arreglaries:

Traceback (most recent call last):
  File "informes.py", line 20, in <module>
    print(mitjana_dies(tasques))
  File "informes.py", line 15, in mitjana_dies
    return sum(t["dies"] for t in tasques) / len(tasques)
ZeroDivisionError: division by zero

Exercici 2: De fràgil a robust

Reescriu aquesta funció perquè sobrevisqui a un fitxer inexistent, a una línia mal formada i a un valor no numèric, registrant cada problema amb logging i retornant el que sí que s'hagi pogut llegir:

def llegir_hores(ruta):
    hores = []
    with open(ruta, encoding="utf-8") as f:
        for linia in f:
            nom, valor = linia.split(",")
            hores.append((nom, int(valor)))
    return hores

Exercici 3: Excepció pròpia

Defineix una excepció PressupostInvalid per a l'Estudi Alba i una funció validar_pressupost(import_, client) que la llanci si l'import no és un número, si és negatiu o si supera els 50.000 € (límit que exigeix l'aprovació de la Marta). Escriu després el try/except que la fa servir mostrant un missatge diferent en cada cas.

Solucions

Solució 1.

L'última línia diu què: una divisió per zero. L'última línia de codi propi diu on: informes.py, line 15, a mitjana_dies. Com que sum(...) no divideix, el zero només pot ser len(tasques): la llista de tasques està buida. L'error real, però, no és a la línia 15 sinó a qui ha cridat des de la línia 20 amb una llista buida; la funció només l'ha manifestat.

def mitjana_dies(tasques: list[dict]) -> float:
    """Retorna la mitjana de dies estimats; 0.0 si no hi ha tasques."""
    if not tasques:                      # LBYL: cas previst i legitim
        return 0.0
    return sum(t["dies"] for t in tasques) / len(tasques)

Si una llista buida fos un cas impossible —un error de programació en qui crida—, el correcte seria el contrari: no capturar-lo i deixar que el ZeroDivisionError faci soroll.

Solució 2.

def llegir_hores(ruta: str) -> list[tuple[str, int]]:
    """Retorna les hores llegides del fitxer, saltant les linies invalides."""
    hores: list[tuple[str, int]] = []
    try:
        linies = Path(ruta).read_text(encoding="utf-8").splitlines()
    except FileNotFoundError:
        logging.error("No existeix el fitxer d hores: %s", ruta)
        return hores                       # llista buida: el programa continua
    for numero, linia in enumerate(linies, start=1):
        try:
            nom, valor = linia.strip().split(",")
            hores.append((nom.strip().lower(), int(valor)))
        except ValueError as error:        # cobreix el split i l int
            logging.warning("Linia %d ignorada: %s", numero, error)
    return hores

Dues idees importants. El try interior embolcalla només les dues línies que poden fallar, i captura ValueError perquè tant un split que no retorna dues parts com un int("vuit") llancen aquesta mateixa excepció. I la fallada d'una línia no avorta el fitxer sencer: es registra amb el seu número i el bucle continua. Aquesta és la diferència entre un programa fràgil i un de robust: continua fent tot el que sí que pot fer.

Solució 3.

LIMIT_APROVACIO = 50000       # a partir d aqui, ho aprova la Marta

class PressupostInvalid(ValueError):
    """L import d un pressupost no compleix les regles de l Estudi Alba."""

def validar_pressupost(import_, client: str) -> float:
    """Retorna l import validat o llanca PressupostInvalid."""
    try:
        import_ = float(import_)
    except (TypeError, ValueError) as error:
        raise PressupostInvalid(f"Import no numeric: {import_!r}") from error
    if import_ < 0:
        raise PressupostInvalid(f"Import negatiu per a {client}: {import_}")
    if import_ > LIMIT_APROVACIO:
        raise PressupostInvalid(f"{import_} EUR supera el limit; ho aprova la Marta.")
    return import_

try:
    total = validar_pressupost("12500", "Forn Sole")
except PressupostInvalid as error:
    print(f"Pressupost rebutjat: {error}")
else:                                    # nomes si no hi ha hagut excepcio
    print(f"Pressupost acceptat: {total:.2f} EUR")

Fixa't en el from error de la primera conversió: conserva la causa original al traceback, de manera que qui depuri sabrà que darrere de PressupostInvalid hi havia un ValueError de float(). Els tres missatges són diferents i diuen què cal fer, que és la marca d'un bon error. I l'else deixa clar que la línia d'èxit només s'executa si no hi ha hagut excepció.

Conclusió

Els errors són de tres tipus: els de sintaxi impedeixen executar, els d'execució llancen excepcions i els de lògica no avisen de res i només es cacen depurant i provant. Davant d'una excepció, el traceback ho explica tot: l'última línia diu què ha passat, l'última línia de codi propi diu on, i l'origen real sol ser més amunt a la pila. Reconèixer el catàleg habitual —ValueError, TypeError, KeyError, IndexError, FileNotFoundError, ZeroDivisionError, AttributeError, json.JSONDecodeError— estalvia la meitat de la feina. Amb try/except es captura l'excepció concreta que saps gestionar, mai except: a seques ni except Exception, col·locant els casos concrets abans que els generals, agrupant-ne diversos en una tupla i recollint l'objecte amb as error; else executa el que només té sentit si no hi ha hagut fallada i finally garanteix la neteja passi el que passi, fins i tot quan l'excepció continua pujant. Amb raise llances errors expressament per fallar aviat i amb un missatge clar, raise a seques rellança sense perdre el traceback i raise ... from conserva la causa. Una excepció pròpia com TascaInvalida, heretada de ValueError, aporta precisió en capturar i missatges del domini. La política es resumeix en una frase: captura només el que puguis resoldre i deixa pujar la resta, preferint EAFP a LBYL quan hi ha fitxers pel mig. Per saber què està passant, logging substitueix el print amb els seus cinc nivells, la seva sortida a fitxer i el seu logging.exception(). I per als errors de lògica, el mètode —reproduir, aïllar, formular hipòtesis, comprovar— juntament amb els punts d'interrupció de VS Code i breakpoint()/pdb.

TascaFàcil és ara la v0.19: un tasques.json corrupte s'aparta amb una còpia en lloc de tombar el programa, una prioritat inventada es rebutja amb TascaInvalida i la interfície torna a preguntar, un dies que arriba com a text es converteix o s'explica, un Ctrl+C desa abans de sortir i tot queda anotat a tascafacil.log. El programa aguanta. Però fixa't en el que acabes de fer: has canviat magatzem.py, model.py, interficie.py i __main__.py alhora, i no hi ha manera de tornar enrere si alguna cosa d'això n'ha empitjorat una altra. No existeix cap còpia de l'estat anterior, ni cap registre de què es va tocar ni per què. Això s'acaba a Control de versions: Git, l'historial del projecte, i la tranquil·litat de poder experimentar sabent que no es perd res.

© Copyright 2026. Tots els drets reservats