Amb el DEFINICIO.md de Definició del projecte i el DISSENY.md de Planificació i disseny sobre la taula, comença la part que esperaves: escriure el programa. I aquí és on es perden la majoria dels primers projectes, no per falta de coneixements —els tens tots— sinó per mètode. Qui comença sol obrir els cinc fitxers alhora, escriure quatre-centes línies sense executar res i trobar-se amb trenta errors encadenats que ja no sap per on agafar. Aquesta lliçó ensenya el contrari: construir en increments petits, amb el programa sempre executable, provant i fent commit a cada pas, i amb un procediment concret per als dos moments difícils —quan alguna cosa falla i no saps per què, i quan et bloqueges i no saps continuar—.

Contingut

  1. L'estratègia: incremental i sempre executable
  2. Ordre de construcció
  3. Fase 1: l'esquelet que arrenca
  4. Fase 2: el model i les seves proves
  5. Fase 3: el magatzem i la seva prova d'anada i tornada
  6. Fase 4: la lògica de la col·lecció
  7. Fase 5: la interfície i la seva prova manual
  8. Quan alguna cosa no funciona: depurar de debò
  9. Gestió dels errors de l'usuari
  10. Commits durant el desenvolupament
  11. Quan et bloqueges
  12. Checklist d'«acabat»
  13. Errors comuns i consells
  14. Exercicis
  15. Conclusió

  1. L'estratègia: incremental i sempre executable

La regla és una sola frase: el programa ha de poder executar-se en tot moment, des del minut u —quan només mostra un menú buit— fins a la v1.0. Mai no hi ha un estat intermedi de «està a mitges, no arrenca». Compara les dues maneres de treballar. En l'enfocament «tot alhora» escrius els cinc mòduls abans d'executar, la primera arrencada treu trenta errors encadenats sense que sàpigues quin causa quin, el commit és gegant i no hi ha res per ensenyar fins al final. En l'enfocament incremental escrius el mínim i executes, cada error apareix sol i sobre el codi que acabes de tocar, hi ha un commit per increment i alguna cosa que funciona des del primer dia.

La diferència de fons és al segon punt. Quan una fallada apareix just després d'escriure deu línies, el sospitós són aquestes deu línies. Quan apareix després de quatre-centes, el sospitós és tot el programa, i depurar passa de cinc minuts a dues hores.

El cicle de treball, doncs, és aquest bucle curt:

flowchart LR
    E["Escriure<br/>un increment petit"] --> R["Executar<br/>python -m lesmevesdespeses"]
    R --> P["Provar<br/>pytest"]
    P --> C["Commit<br/>missatge descriptiu"]
    C --> E
    R -->|falla| D["Depurar"]
    D --> R
    P -->|falla| D

Una volta completa d'aquest bucle hauria de durar entre quinze minuts i una hora: si portes tres hores sense tornar a la casella «Executar», l'increment era massa gran.

  1. Ordre de construcció

L'ordre no és indiferent: es construeix de dins cap a fora, perquè cada capa depèn de l'anterior i perquè les capes interiors són les que es poden provar automàticament.

Fase Què construeixes Com ho comproves Est.
1. Esquelet __main__.py i interficie.py amb el menú i la sortida Arrenca i surt 1 h
2. Model model.py: entitat, validació, excepció pytest tests/test_model.py 2 h
3. Magatzem magatzem.py: desar i carregar pytest amb tmp_path 2 h
4. Lògica quadern.py: afegir, cercar, filtrar, totals pytest tests/test_quadern.py 3 h
5. Interfície Les opcions del menú, una a una Guió de prova manual 6 h
6. Extres Should, robustesa, logging, format Guió + revisió final 4 h

Dos advertiments sobre aquest ordre. El primer: encara que la interfície sigui l'últim de la llista, la fita de la primera funcionalitat completa de cap a cap arriba abans d'acabar totes les capes. Tan bon punt tinguis model, magatzem i un afegir a la lògica, fes l'opció «registrar» del menú sencera; veure el programa fent alguna cosa real canvia la motivació més que qualsevol altra cosa. I el segon: les fases 2, 3 i 4 es proven automàticament; la 5, no. Provar una interfície de línia d'ordres automàticament és possible però costós, i no és el que correspon a un primer projecte: per a ella es fa servir el guió manual de l'apartat 7.

  1. Fase 1: l'esquelet que arrenca

El primer objectiu és que python -m lesmevesdespeses funcioni. Res més.

# lesmevesdespeses/interficie.py -- capa de presentacio
OPCIONS = {"1": "Registrar despesa", "2": "Registrar ingres", "3": "Moviments del mes",
           "4": "Resum per categoria", "5": "Esborrar moviment", "0": "Sortir"}


def executar() -> None:
    """Bucle principal de l'aplicacio."""
    while True:
        print("\n=== LesMevesDespeses ===")
        for clau, text in OPCIONS.items():
            print(f"  {clau}. {text}")
        opcio = input("Opcio: ").strip()
        if opcio == "0":
            print("Fins aviat.")
            break
        print(f"[pendent] {OPCIONS[opcio]}" if opcio in OPCIONS else "Opcio no valida.")

# lesmevesdespeses/__main__.py  ->  from .interficie import executar; executar()

Són vint línies i cap no fa res útil encara, però resolen de cop els tres problemes que més bloquegen al principi: que el paquet s'importi bé, que l'execució amb -m funcioni i que existeixi el bucle principal de Menús interactius. El [pendent] de cada opció és deliberat: el menú complet existeix des del primer dia i les opcions es van omplint una a una. Commit: Esquelet: menu principal que arrenca i surt.

  1. Fase 2: el model i les seves proves

Ara l'entitat, amb tota la seva validació a dins. És la peça més important del programa: si el model garanteix que no pot existir un moviment invàlid, cap altra capa no s'ha de preocupar de comprovar-ho.

# lesmevesdespeses/model.py -- entitats del domini i les seves regles de validacio
CATEGORIES = ("menjar", "transport", "oci", "llar", "salut", "ingressos", "altres")


class MovimentInvalid(ValueError):
    """Es llanca quan les dades d'un moviment no compleixen les regles."""


@dataclass
class Moviment:
    """Un ingres o una despesa. El signe de la quantitat distingeix l'un de l'altra."""

    data: date
    concepte: str
    quantitat: float
    categoria: str
    id: int = 0

    def __post_init__(self) -> None:
        self.concepte = self.concepte.strip()              # normalitzar...
        self.categoria = self.categoria.strip().lower()
        if not self.concepte or len(self.concepte) > 60:   # ...i despres validar
            raise MovimentInvalid("El concepte ha de tenir entre 1 i 60 caracters.")
        if self.quantitat == 0:
            raise MovimentInvalid("L'import no pot ser zero.")
        if self.categoria not in CATEGORIES:
            raise MovimentInvalid(f"Categoria desconeguda: {self.categoria!r}.")
        if self.data > date.today():
            raise MovimentInvalid("La data no pot ser futura.")

    def es_despesa(self) -> bool:
        return self.quantitat < 0

    def __str__(self) -> str:                      # linia ja formatada de taula
        return f"{self.id:>4}  {self.data}  {self.concepte:<30} {self.quantitat:>10.2f}"

Les decisions que hi ha aquí, una a una. @dataclass (07-02) evita escriure un __init__ que només assignaria cinc atributs. __post_init__ és el mètode que dataclass crida just després de construir, i és on va la validació. Es normalitza abans de validarstrip() i lower()— perquè « Menjar » i «menjar» siguin el mateix, que era un dels casos límit previstos a 09-01. L'excepció pròpia hereta de ValueError perquè és un error de valor: qui no conegui MovimentInvalid pot capturar ValueError i funcionarà igual. I __str__ retorna ja la línia formatada de taula, amb les amplades fixes de l'esbós de pantalla.

Les proves, amb el patró AAA de Proves automatitzades:

# tests/test_model.py
def test_normalitza_concepte_i_categoria():
    m = Moviment(date(2026, 8, 1), "  Cafe  ", -1.5, " MENJAR ")
    assert m.concepte == "Cafe" and m.categoria == "menjar" and m.es_despesa()


@pytest.mark.parametrize("concepte, quantitat, categoria", [
    ("", -10.0, "menjar"),          # concepte buit
    ("Compra", 0.0, "menjar"),      # quantitat zero
    ("Compra", -10.0, "viatges"),   # categoria desconeguda
])
def test_dades_invalides(concepte, quantitat, categoria):
    with pytest.raises(MovimentInvalid):
        Moviment(date(2026, 8, 1), concepte, quantitat, categoria)


def test_data_futura_rebutjada():
    dema = date.today() + timedelta(days=1)       # calculada, mai fixa
    with pytest.raises(MovimentInvalid):
        Moviment(dema, "Compra", -10.0, "menjar")

Fixa't en què es prova: un cas bo, la normalització i tots els casos dolents. Els tres invàlids van en un parametrize perquè són la mateixa prova amb dades diferents, i pytest els compta com a tres proves separades, així que si en falla una saps quina. La de la data futura calcula «demà» en lloc d'escriure una data fixa: una prova amb date(2027, 1, 1) començaria a fallar sola el 2027, i una prova que caduca és pitjor que no tenir-la. Commit: Model Moviment amb validacio i proves (RF-1).

  1. Fase 3: el magatzem i la seva prova d'anada i tornada

desar és la part fàcil: construeix el diccionari amb l'estructura que vas fixar a 09-02 —versio, seguent_id i la llista de moviments convertits amb m.data.isoformat()— i l'escriu amb ruta.write_text(json.dumps(dades, indent=2, ensure_ascii=False), encoding="utf-8"). La part que cal fer bé és la lectura:

# lesmevesdespeses/magatzem.py   (RUTA = Path("despeses.json"), logger = getLogger(__name__))

def carregar(ruta: Path = RUTA) -> tuple[list[Moviment], int]:
    """Llegeix els moviments desats. Retorna llista buida si no hi ha fitxer."""
    if not ruta.exists():
        return [], 1                       # primera arrencada: no es un error
    try:
        dades = json.loads(ruta.read_text(encoding="utf-8"))
    except json.JSONDecodeError:
        logger.error("Fitxer de dades corrupte: %s", ruta)
        return [], 1                       # es registra i es continua, no es mor
    moviments = [
        Moviment(date.fromisoformat(d["data"]), d["concepte"],
                 d["quantitat"], d["categoria"], id=d["id"])
        for d in dades.get("moviments", [])
    ]
    return moviments, dades.get("seguent_id", len(moviments) + 1)

El que cal mirar aquí és la tolerància a fallades, exactament igual que al magatzem.py de TascaFàcil: el fitxer absent és la primera arrencada i el corrupte es registra al log (08-02) en lloc de matar el programa. I el paràmetre ruta amb valor per defecte no és un caprici: és el que permet que les proves facin servir un fitxer temporal.

# tests/test_magatzem.py
def test_cicle_desar_i_carregar(tmp_path):
    ruta = tmp_path / "dades.json"
    originals = [Moviment(date(2026, 8, 1), "Compra", -62.35, "menjar", id=1),
                 Moviment(date(2026, 8, 3), "Nomina", 1450.0, "ingressos", id=2)]
    desar(originals, seguent_id=3, ruta=ruta)

    recuperats, seguent = carregar(ruta)

    assert seguent == 3 and len(recuperats) == 2
    assert recuperats[0].quantitat == -62.35        # continua sent float
    assert recuperats[1].data == date(2026, 8, 3)   # continua sent date


def test_fitxer_inexistent_no_falla(tmp_path):
    assert carregar(tmp_path / "no_existeix.json") == ([], 1)


def test_fitxer_corrupte_no_falla(tmp_path):
    (tmp_path / "trencat.json").write_text("{no es json", encoding="utf-8")
    assert carregar(tmp_path / "trencat.json")[0] == []

La primera és la prova d'anada i tornada: deso, carrego i comprovo que el que he recuperat és idèntic a l'original. És la prova més rendible de tota la persistència, perquè un sol assert detecta fallades de serialització, de tipus i d'estructura. La fixture tmp_path de pytest dóna un directori temporal diferent per prova i el esborra en acabar, així que les proves no toquen mai les teves dades reals ni s'estorben entre si. Commit: Magatzem JSON tolerant a fitxer absent o corrupte (RF-6).

  1. Fase 4: la lògica de la col·lecció

# lesmevesdespeses/quadern.py -- colleccio de moviments i operacions sobre ella


class Quadern:
    """Desa els moviments i respon consultes sobre ells."""

    def __init__(self, moviments: list[Moviment] | None = None, seguent_id: int = 1):
        self._moviments = moviments or []
        self.seguent_id = seguent_id

    def afegir(self, moviment: Moviment) -> Moviment:
        """Assigna identificador al moviment i l'afegeix al quadern."""
        moviment.id = self.seguent_id
        self.seguent_id += 1
        self._moviments.append(moviment)
        return moviment

    def del_mes(self, mes: str) -> list[Moviment]:
        """Moviments d'un mes 'AAAA-MM', ordenats per data."""
        del_mes = [m for m in self._moviments if m.data.isoformat().startswith(mes)]
        return sorted(del_mes, key=lambda m: m.data)

    def total_per_categoria(self, mes: str) -> dict[str, float]:
        """Suma les quantitats de cada categoria del mes, de mes despesa a menys."""
        totals: dict[str, float] = {}
        for m in self.del_mes(mes):
            totals[m.categoria] = totals.get(m.categoria, 0.0) + m.quantitat
        return dict(sorted(totals.items(), key=lambda parell: parell[1]))

    def saldo(self, mes: str) -> float:
        """Diferencia entre ingressos i despeses del mes."""
        return round(sum(m.quantitat for m in self.del_mes(mes)), 2)

És la traducció literal del pseudocodi de 09-02 (falten __len__, __iter__, cercar —un next() sobre la llista que retorna None si no hi ha coincidència— i eliminar, que fa servir cercar i retorna True o False), i val la pena assenyalar tres coses. total_per_categoria reutilitza del_mes en lloc de repetir el filtre: una responsabilitat, un sol lloc. El key=lambda parell: parell[1] ordena per valor, i com que les despeses són negatives, l'ordre ascendent posa primer la despesa més gran, que és el que volia l'esbós de pantalla (06-02 i 04-05 treballant plegats). I el round(..., 2) de saldo evita que tregui el nas l'aritmètica de coma flotant: sense ell, -62.35 + 1450.0 pot imprimir 1387.6500000000001.

Les proves d'aquesta capa se centren en els càlculs, que és on de debò pot haver-hi un error. I aquí no cal inventar res, perquè cada taula de prova d'escriptori de 09-02 es converteix en un def test_: els mateixos quatre moviments d'aquella taula, el mateix mes i el mateix resultat esperat.

def test_total_per_categoria_agrupa_i_filtra_el_mes():
    q = Quadern()
    q.afegir(Moviment(date(2026, 7, 30), "Juliol", -20.0, "menjar"))    # un altre mes
    q.afegir(Moviment(date(2026, 8, 1), "Compra", -62.35, "menjar"))
    q.afegir(Moviment(date(2026, 8, 4), "Sopar", -15.0, "menjar"))
    q.afegir(Moviment(date(2026, 8, 1), "Abonament", -32.0, "transport"))

    totals = q.total_per_categoria("2026-08")

    assert totals == {"menjar": -77.35, "transport": -32.0}
    assert list(totals)[0] == "menjar"   # la despesa mes gran va primer

Commit: Quadern: alta, baixa, filtre per mes i totals (RF-3, RF-4, RF-5).

  1. Fase 5: la interfície i la seva prova manual

La interfície s'omple una opció cada vegada, i cada opció s'acaba del tot abans de començar la següent. La seva regla és la de 04-04: aquí es fa input i print, i res més; cap càlcul del domini.

def registrar_despesa(quadern: Quadern) -> None:
    """Opcio 1 del menu: dona d'alta una despesa."""
    concepte = input("Concepte: ")
    quantitat = demanar_quantitat("Import (EUR): ")
    categoria = input(f"Categoria {CATEGORIES}: ")
    text_data = input("Data AAAA-MM-DD (buit = avui): ").strip()
    try:
        data = date.fromisoformat(text_data) if text_data else date.today()
        moviment = quadern.afegir(Moviment(data, concepte, -quantitat, categoria))
    except (ValueError, MovimentInvalid) as error:
        print(f"No s'ha registrat: {error}")
        return
    print(f"Despesa registrada (id {moviment.id}).")

Tres detalls amb intenció. demanar_quantitat (un bucle while True amb try/except ValueError que només retorna quan el valor és més gran que zero) insisteix fins a obtenir un número vàlid en lloc de rendir-se (02-04), i fa .replace(",", ".") per acceptar la coma decimal, que era un altre cas límit previst. Es demana l'import en positiu i es desa com a -quantitat: a l'usuari no se li pot exigir que entengui el conveni de signes del disseny. I el try/except captura tant ValueError (data mal escrita) com MovimentInvalid (regles del model) i torna al menú amb un missatge, complint el RNF-1.

Com que això no es prova automàticament, es prova amb un guió escrit que executes sencer abans de donar per bona cada sessió de treball:

# Què faig Què ha de passar OK?
1 Esborro despeses.json i arrenco Arrenca sense error, quadern buit
2 Opció 3 sense dades «Sense moviments a 2026-08», no una taula buida
3 Registro una despesa correcta Confirma amb id 1
4 Registro amb import dotze i després 12,50 Torna a demanar-ho sense trencar-se; després l'accepta com a 12,50
5 Registro amb categoria viatges Missatge de categoria desconeguda, torna al menú
6 Registro amb data 2026-02-31 Missatge de data invàlida, torna al menú
7 Surto i torno a entrar Els moviments continuen allà
8 Esborro l'id 99 i després un de real «No existeix el moviment 99» sense traceback; el real desapareix del fitxer
9 Opció 7 i després abc «Opció no vàlida» en tots dos casos
10 Opció 4 amb dades de dos mesos Només suma el mes demanat; els percentatges quadren a 100 %

Aquest guió és la teva xarxa de seguretat per al que les proves automàtiques no cobreixen. Desa'l a tests/guio_manual.md i recorre'l sencer abans de cada etiqueta de versió.

  1. Quan alguna cosa no funciona: depurar de debò

Tard o d'hora alguna cosa falla i no saps per què. Aplica el mètode de Depuració i gestió d'errors a un cas real: el resum d'agost mostra un percentatge del 137 %.

Pas Què es fa En aquesta fallada concreta
1. Reproduir Trobar la recepta exacta que el provoca sempre Amb dades_exemple.json, opció 4, mes 2026-08
2. Llegir l'error El traceback es llegeix de baix a dalt: tipus, fitxer i línia; la primera línia dels teus fitxers és la sospitosa Aquí no hi ha traceback: és un resultat incorrecte, el cas més difícil
3. Hipòtesi concreta Una frase falsable, no «alguna cosa falla» «El denominador inclou els ingressos, que són positius, i per això el total és menor del que hauria de ser»
4. Comprovar-la Punt d'interrupció a la línia sospitosa i inspecció de variables totals inclou "ingressos": 1450.0 i total_despeses val 892.9: confirmada
5. Arreglar la causa Mai el símptoma sum(totals.values())sum(v for v in totals.values() if v < 0)
6. Prova de regressió La prova que hauria fallat abans de l'arreglada assert total_despeses == -100.0 amb dues despeses i un ingrés

El pas 5 mereix èmfasi: l'arreglada dolenta hauria estat restar 1450 en algun lloc perquè el número quadrés. Hauria funcionat amb aquelles dades i hauria tornat a fallar amb unes altres. I el pas 6 no és opcional: una fallada arreglada sense prova torna, normalment a la refactorització següent. Commit: Corregeix el percentatge del resum, que incloia els ingressos.

  1. Gestió dels errors de l'usuari

Abans de tancar la implementació, fes un repàs sistemàtic: llista totes les entrades de l'usuari i decideix per a cadascuna què fas. Hi ha dues estratègies i convé no confondre-les. Validar és comprovar abans d'actuar i tornar a demanar la dada: serveix per al que l'usuari pot corregir. Capturar és deixar que l'operació falli i atrapar l'excepció amb try/except: serveix per al que no es pot comprovar per avançat sense duplicar la feina.

Entrada Risc Estratègia On
Opció del menú Text o número fora de rang Validar contra el diccionari interficie.executar
Import No numèric, zero, negatiu, amb coma Validar en bucle interficie.demanar_quantitat
Data i mes Format o dia impossible Capturar ValueError interficie.registrar_despesa
Categoria Desconeguda, majúscules, espais Normalitzar i validar model.Moviment
Id a esborrar No numèric o inexistent Capturar + comprovar None interficie + quadern.cercar
Fitxer de dades Absent, corrupte, sense permisos Capturar i arrencar buit magatzem.carregar

La regla d'or és a l'última columna: la validació de les regles del domini viu al model, la del format d'entrada viu a la interfície. Si barreges les dues, acabes validant la categoria en tres llocs diferents i oblidant-te'n d'un.

  1. Commits durant el desenvolupament

Un commit és una unitat de treball amb sentit: alguna cosa que funciona i que podries explicar en una frase. Ni cada Ctrl+S ni una vegada per setmana. A la pràctica, un per volta del bucle de l'apartat 1.

$ git log --oneline
a91c02f Afegeix exportacio a CSV del mes (RF-7)
7d4e1b8 Corregeix el percentatge del resum, que incloia els ingressos
3f2a90c Afegeix pantalla de resum per categoria (RF-4)
c18b7e5 Quadern: alta, baixa, filtre per mes i totals (RF-3, RF-5)
9e04a13 Magatzem JSON tolerant a fitxer absent o corrupte (RF-6)
2a1f6e0 Esquelet: menu principal que arrenca i surt

Aquest historial es llegeix com el diari del projecte: cada línia és un increment, cada missatge comença per un verb i uns quants citen el requisit que implementen. Tres regles pràctiques: no facis mai commit amb les proves en vermell (si necessites desar a mitges, fes servir una branca), no barregis refactorització amb funcionalitat nova al mateix commit (08-05), i no desis dades personals ni l'entorn virtual, que per això hi ha el .gitignore de 09-02.

  1. Quan et bloqueges

Bloquejar-se és normal i li passa a tothom. El que distingeix qui avança és tenir un procediment en lloc de mirar la pantalla:

  1. L'ànec de goma. Explica el problema en veu alta, línia a línia, a un objecte inanimat. Sona ridícul i funciona: en obligar-te a verbalitzar el que creus que fa el codi, trobes el punt on el que creus i el que passa no coincideixen.
  2. Redueix a un exemple mínim i divideix. Treu el tros sospitós a un fitxer nou de deu línies amb dades inventades: o la fallada desapareix —i la causa és al context que has tret— o es reprodueix en deu línies, que ja saps depurar. Amb assert o print localitza el punt exacte en què les dades deixen de ser el que esperes; la fallada és entre l'últim punt correcte i el primer incorrecte.
  3. Busca el missatge exacte. Copia el text de l'error sense els teus noms de variable: TypeError: unsupported operand type(s) for -: 'str' and 'int', no error a la meva funcio resum. La cerca del missatge literal gairebé sempre encerta el cas.
  4. Llegeix la documentació oficial. docs.python.org té la resposta a qualsevol dubte sobre la biblioteca estàndard, amb exemples. Un tutorial d'un blog et dóna una recepta; la documentació et dóna el model mental, i aquest serveix per als vint dubtes següents.
  5. Descansa. Després de quaranta minuts encallat, la probabilitat de resoldre-ho cau en picat. Aixecar-se vint minuts és la tècnica de depuració més eficaç que existeix i la més difícil d'aplicar.

Sobre els assistents d'IA: són una eina legítima i seran a la teva vida professional, però en el teu primer projecte poden robar-te justament l'aprenentatge que busques. que expliquin un missatge d'error, que preguntis per què una funció es comporta com ho fa, que demanis una revisió del teu codi ja escrit i que t'aclareixin un concepte. No a «fes-me el projecte» ni a enganxar codi que no entens: un projecte que no saps explicar (09-04) no et serveix ni en una entrevista ni quan calgui arreglar-lo. Regla pràctica: intenta el problema vint minuts pel teu compte abans de preguntar, i no enganxis mai codi que no sabries reescriure de memòria en allò essencial.

  1. Checklist d'«acabat»

Abans de donar per tancada la implementació i passar a la presentació:

# Comprovació Fet?
1 Tots els requisits Must funcionen de cap a cap
2 El guió de prova manual passa sencer
3 pytest en verd, amb proves de model, lògica i persistència
4 Primera arrencada sense fitxer de dades: funciona
5 Cap entrada de l'usuari no produeix un traceback i els casos límit de 09-01 estan comprovats
6 black . i ruff check . sense queixes
7 Sense codi mort, print de depuració ni TODO oblidats
8 Tot committejat; git status net
9 El programa arrenca en una carpeta acabada de clonar

La 9 és la que més falla i la que més vergonya fa en una demostració: el programa funcionava només perquè a la teva carpeta hi havia un fitxer que no era al repositori. Prova-ho de debò: clona en un altre directori i executa.

Errors Comuns i Consells

  • Escriure-ho tot abans d'executar res. L'error clàssic. Trenta errors encadenats no es depuren, s'abandonen. Executa cada quinze minuts.
  • Deixar la validació per al final. «Ja posaré els try/except quan funcioni» acaba amb un programa que es trenca a la demostració. La validació s'escriu amb la funció, no després.
  • Perseguir el símptoma. Restar un número màgic perquè el total quadri tapa la fallada, que tornarà amb unes altres dades: arregla la causa i escriu la prova de regressió.
  • Abandonar les proves a mitges. Quan pitgen les ganes d'avançar, el primer que cau són les proves, i justament aleshores és quan comencen a fer falta. Prova almenys model i càlculs. I no barregis mai refactorització amb funcionalitat nova al mateix commit: si alguna cosa es trenca, no sabràs quina de les dues va ser.
  • Consell: acaba cada sessió amb el programa funcionant i committejat, i deixa un SEGUENT.md de dues línies amb el que ve. Tornar a un projecte trencat és la millor manera de no tornar-hi, i aquestes dues línies t'estalvien vint minuts de recontextualització a cada sessió.

Exercicis

Són les següents tasques reals del teu projecte. En acabar, hauries de tenir la implementació tancada.

Exercici 1: Esquelet i model provat

Implementa la fase 1 (el programa arrenca, mostra el menú i surt) i la fase 2 (la teva entitat principal amb validació completa i excepció pròpia). Escriu tests/test_model.py amb almenys sis proves: un cas vàlid, la normalització de dades i quatre casos invàlids, fent servir parametrize per als que comparteixin estructura. Fes un commit per fase.

Exercici 2: Persistència amb prova d'anada i tornada

Implementa el magatzem: desar i carregar en el format que vas decidir a 09-02, tolerant a fitxer absent i corrupte. Escriu les tres proves amb tmp_path: cicle complet d'anada i tornada, fitxer inexistent i fitxer corrupte. Després connecta el magatzem a l'esquelet: en arrencar carrega, en sortir desa.

Exercici 3: Un requisit de cap a cap i el seu guió

Tria el teu requisit funcional principal (l'equivalent al RF-1) i implementa'l complet: opció del menú, entrada validada, crida a la lògica, persistència i confirmació en pantalla. Després escriu el teu guió de prova manual amb un mínim de deu comprovacions que incloguin els casos límit de 09-01, i executa'l sencer anotant el que falli.

Solucions

Solució 1. Els apartats 3 i 4 contenen la solució per a LesMevesDespeses. Contrasta el teu model amb aquests criteris: la validació és dins de l'entitat i no a la interfície, la normalització passa abans que la validació, l'excepció pròpia hereta de ValueError, i cap prova no fa servir una data fixa que pugui caducar. Rúbrica: el teu model fa impossible construir un objecte invàlid? Tens almenys una prova per regla de validació? pytest passa en menys d'un segon?

Solució 2. Apartat 5. Els tres punts que solen fallar: que la ruta sigui un paràmetre amb valor per defecte (sense això no pots fer servir tmp_path), que el fitxer absent retorni buit en lloc de llançar, i que els tipus sobrevisquin al viatge —una data ha de tornar com a date i un import com a float, no com a cadenes—. Rúbrica: la prova d'anada i tornada comprova valors concrets i no només la longitud de la llista? Les proves fan servir tmp_path i no toquen el teu fitxer real? La primera arrencada sense fitxer funciona?

Solució 3. Apartat 7, amb registrar_despesa i la taula de deu comprovacions. L'essencial no és el codi sinó el repartiment: la interfície demana i mostra, el model valida, el quadern desa en memòria, el magatzem escriu. Si la teva funció d'alta calcula alguna cosa del domini o el teu model imprimeix, la separació de capes s'ha trencat. Rúbrica: el teu guió inclou el cas de llista buida, el d'entrada no numèrica i el d'identificador inexistent? Passa sencer, sense ni un sol traceback? Has fet commit del requisit citant-ne el número?

Conclusió

Implementar bé és, sobretot, una qüestió de mètode. L'estratègia és incremental i el programa està sempre executable: primer l'esquelet que arrenca i surt, després una funcionalitat completa de cap a cap i només llavors la següent, girant cada quinze o seixanta minuts el bucle escriure → executar → provar → commit. L'ordre de construcció va de dins cap a fora —model amb les seves proves, magatzem amb la seva prova d'anada i tornada, lògica de la col·lecció, interfície opció a opció i extres al final— perquè les capes interiors són les que es proven automàticament i les que sostenen tota la resta.

Les proves s'escriuen amb el codi, no després: un cas bo, la normalització i tots els casos dolents al model, amb parametrize per als que comparteixen estructura; el cicle desar/carregar i els fitxers absent i corrupte al magatzem amb tmp_path; els càlculs a la lògica, convertint cada prova d'escriptori de 09-02 en un def test_; i per a la interfície, un guió manual escrit que es recorre sencer abans de cada versió. Quan alguna cosa falla, el mètode de 08-02 en sis passos —reproduir, llegir, formular una hipòtesi concreta, comprovar-la amb el depurador, arreglar la causa i escriure la prova de regressió— converteix un misteri en una tasca de vint minuts. Els errors de l'usuari es reparteixen entre validar (el corregible, a la interfície) i capturar (l'imprevisible, amb try/except), amb les regles del domini sempre al model. Els commits marquen cada increment amb un verb i el requisit implementat, mai en vermell i mai barrejant refactorització amb funcionalitat. I per als bloquejos hi ha procediment: ànec de goma, exemple mínim, buscar el missatge exacte, documentació oficial, dividir i provar, i descansar —amb els assistents d'IA fets servir per entendre i revisar, mai per substituir-te, i sempre després de vint minuts pel teu compte—. La checklist d'acabat, amb la seva comprovació final de clonar en una carpeta neta i arrencar, tanca la implementació.

Arribats aquí tens un programa que funciona, està provat i viu en un repositori endreçat. Falta el que converteix un projecte en alguna cosa que compta per als altres: saber-lo ensenyar. A Presentació del projecte deixarem el repositori presentable, escriurem el README com a carta de presentació, prepararem una demostració de cinc minuts i aprendrem a defensar les decisions tècniques i a parlar de les limitacions sense restar-nos valor.

© Copyright 2026. Tots els drets reservats