El mòdul 5 va acabar amb la confessió més dolorosa de l'Anna: cada nit, en apagar l'ordinador de Papyrus, el catàleg sencer — objectes Llibre amb les seves validacions, els socis, les línies de venda — s'evapora. La RAM és ràpida però oblidadissa: quan el programa acaba, les seves variables moren amb ell. La solució és el disc: escriure les dades en fitxers que sobreviuen a les reinicialitzacions i llegir-los en arrencar. En aquesta lliçó aprendràs open() i els seus modes, la manera correcta de treballar amb fitxers (with), les diferents maneres de llegir i escriure text, i per què encoding="utf-8" t'ha d'acompanyar sempre. De passada saldarem un deute que el curs arrossega des del mòdul 1: fitxa_llibre.py per fi desarà les altes en un fitxer.

Contingut

  1. Persistència: per què la memòria no és suficient
  2. open(): la porta al disc
  3. Els modes d'obertura: r, w, a, x
  4. with: la manera correcta d'obrir fitxers
  5. Escriure: write() i writelines()
  6. Llegir: read(), readline(), readlines() i iterar línia a línia
  7. encoding="utf-8": sempre i explícit
  8. El salt de línia: \n en escriure, strip() en llegir
  9. Promesa saldada: fitxa_llibre.py escriu a registre_altes.txt
  10. Errors comuns i consells
  11. Exercicis amb solucions

Persistència: per què la memòria no és suficient

Tot el que has creat fins ara viu a la memòria RAM del procés de Python: existeix mentre el programa s'executa i desapareix quan acaba. Aquesta propietat de les dades de sobreviure entre execucions s'anomena persistència, i la manera més universal d'aconseguir-la és el sistema de fitxers: text pla avui, CSV a 06-02, JSON a 06-03.

flowchart LR
    A["RAM<br>cataleg: dict[str, Llibre]<br>ràpida, volàtil"] -- "escriure (aquesta lliçó)" --> B["Disc<br>cataleg.txt, registre_altes.txt<br>lent, persistent"]
    B -- "llegir (aquesta lliçó)" --> A

El pla del mòdul és exactament aquest cicle: desar en tancar, carregar en obrir. Papyrus deixarà de llevar-se amnèsica.

open(): la porta al disc

La funció integrada open() connecta el teu programa amb un fitxer del disc i retorna un objecte fitxer amb mètodes per llegir i escriure:

fitxer = open("salutacio.txt", "w", encoding="utf-8")  # obre (creant-lo) per escriure
fitxer.write("Benvinguts a Papyrus\n")                 # escriu una cadena
fitxer.close()                                         # imprescindible! allibera el fitxer

Els tres arguments que faràs servir sempre:

  • Ruta: "salutacio.txt" és una ruta relativa — es resol des de la carpeta on executes el programa. Les rutes a fons (absolutes, pathlib) arriben a 06-04; de moment treballa amb fitxers al costat del teu script.
  • Mode: "w", "r", "a"... — la secció següent.
  • encoding: com es tradueixen els caràcters a bytes. Sempre "utf-8", i a la seva secció veuràs per què no és negociable.

close() és crític: fins que no tanques, el que has escrit pot quedar-se en una memòria intermèdia sense arribar al disc, i el fitxer queda bloquejat per a altres programes. I aquí hi ha el problema: si entre open() i close() alguna cosa falla, el close() no s'arriba a executar. Per això gairebé mai escriuràs open()/close() a mà — de seguida arriba with.

Els modes d'obertura: r, w, a, x

El segon argument d'open() decideix què pots fer amb el fitxer i què passa si existeix o no:

Mode Significat Si el fitxer existeix Si NO existeix Ús típic a Papyrus
"r" read — llegir (és el mode per defecte) L'obre per llegir Error (FileNotFoundError) Carregar el catàleg en arrencar
"w" write — escriure El buida del tot i escriu des de zero El crea Abocar el catàleg sencer en tancar
"a" append — afegir Escriu al final, conservant l'anterior El crea Registre d'altes: cada alta, una línia nova
"x" exclusive — crear en exclusiva Error (FileExistsError) El crea Evitar aixafar un fitxer per accident

Dues advertències abans de continuar:

  • "w" és destructiu: tan bon punt executes open("cataleg.txt", "w", ...), el contingut anterior ja s'ha perdut — encara que després no escriguis res. Triar "w" on tocava "a" és l'error més car d'aquesta lliçó.
  • Obrir amb "r" un fitxer que no existeix llança FileNotFoundError. La gestió elegant d'errors (try/except) és el tema central del mòdul 7; fins llavors farem servir un pal·liatiu explícit — comprovar-ho abans amb Path.exists():
from pathlib import Path   # pathlib protagonitzara 06-04; avui nomes li demanem exists()

if Path("cataleg.txt").exists():
    ...   # es segur llegir-lo
else:
    print("Encara no hi ha cataleg desat; comencem de zero.")

Que quedi dit amb claredat: aquesta comprovació prèvia és un pedaç temporal i té esquerdes (el fitxer podria desaparèixer entre la comprovació i la lectura). L'eina correcta són les excepcions, i les aprendràs al mòdul 7.

També existeixen els modes binaris ("rb", "wb") per a bytes en cru — imatges, o el pickle que esmentarem a 06-03 —, però en aquest mòdul treballem amb text.

with: la manera correcta d'obrir fitxers

La versió professional del primer exemple no porta close():

with open("salutacio.txt", "w", encoding="utf-8") as fitxer:
    fitxer.write("Benvinguts a Papyrus\n")
# en sortir del bloc, el fitxer JA esta tancat — passi el que passi a dins

with garanteix el tancament sempre: si el bloc acaba bé, tanca; si a dins salta un error a mig camí, tanca igualment abans de propagar l'error. És l'equivalent a un close() impossible d'oblidar. La variable després d'as (fitxer) és el mateix objecte fitxer d'abans, però amb el cicle de vida gestionat.

with funciona amb fitxers perquè aquests implementen el protocol dels context managers — un mecanisme general de Python que també serveix per a connexions, bloquejos i recursos propis. Aquest protocol complet (i com escriure els teus) s'estudia al mòdul 8; en aquest mòdul en tens prou amb la regla pràctica: tot open() va dins d'un with. Així ho farem a la resta del curs sense excepció.

Escriure: write() i writelines()

write(cadena) escriu exactament la cadena que li passes — ni més ni menys. Dues conseqüències immediates:

  • No afegeix salt de línia: si vols línies, escriu tu el "\n".
  • Només accepta cadenes: els números cal convertir-los (les f-strings del mòdul 1 són perfectes per a això).
llibres = [("L'Odissea", 12.50, 4), ("Hamlet", 9.95, 6), ("El Quixot", 15.90, 8), ("Faust", 21.00, 0)]

with open("cataleg.txt", "w", encoding="utf-8") as f:
    f.write("Cataleg de Papyrus\n")            # el \n el poses tu
    for titol, preu, estoc in llibres:
        f.write(f"{titol} | {preu:.2f} | {estoc}\n")   # numeros → text via f-string

Contingut resultant de cataleg.txt:

Cataleg de Papyrus
L'Odissea | 12.50 | 4
Hamlet | 9.95 | 6
El Quixot | 15.90 | 8
Faust | 21.00 | 0

writelines(llista) escriu totes les cadenes d'una llista... i, malgrat el nom, tampoc no afegeix salts de línia. És útil quan ja tens les línies preparades:

linies = [f"{t} | {p:.2f} | {s}\n" for t, p, s in llibres]   # comprensio (02-04), \n inclos
with open("cataleg.txt", "w", encoding="utf-8") as f:
    f.write("Cataleg de Papyrus\n")
    f.writelines(linies)

Fixa't en el separador |: l'hem inventat nosaltres, i funciona… fins que un títol contingui una barra vertical. Inventar formats amb separadors casolans és fràgil — exactament el problema que el format CSV i el seu mòdul estàndard resolen a 06-02.

Llegir: read(), readline(), readlines() i iterar línia a línia

Per llegir tens quatre camins; triar bé depèn de la mida del fitxer:

Mètode Retorna Memòria Quan fer-lo servir
f.read() TOT el contingut en una sola cadena Tot el fitxer alhora Fitxers petits que processes com un bloc
f.readline() La línia següent (amb el seu \n) Una línia Llegir una capçalera i parar
f.readlines() Llista amb totes les línies (amb \n) Tot el fitxer alhora Necessites la llista completa (comptar, indexar)
for linia in f: Una línia per volta Una línia cada vegada El mode per defecte: processar línia a línia
with open("cataleg.txt", "r", encoding="utf-8") as f:
    contingut = f.read()          # una unica cadena amb els \n inclosos
print(len(contingut))

Per a un cataleg.txt de cinc línies qualsevol mètode val. Però imagina l'històric de vendes de deu anys de Papyrus: read() i readlines() el carregarien sencer a la RAM. Iterar sobre el fitxer, en canvi, llegeix una línia, la processa, l'oblida i passa a la següent — memòria constant, tant li fa que el fitxer tingui cinc línies com cinc milions:

with open("cataleg.txt", "r", encoding="utf-8") as f:
    next(f)                                   # salta la capcalera "Cataleg de Papyrus"
    for linia in f:                           # l'objecte fitxer es iterable (02-02)
        titol, preu, estoc = linia.strip().split(" | ")   # 04-05: strip + split
        print(f"{titol}: {float(preu):.2f} € — estoc {int(estoc)}")

Observa les conversions float(preu) i int(estoc): un fitxer de text només conté text. El 12.50 que vas escriure com a número torna com la cadena "12.50", i recuperar els tipus és responsabilitat teva. Aquest "peatge de conversió" reapareixerà a CSV (06-02) i desapareixerà gairebé del tot amb JSON (06-03).

encoding="utf-8": sempre i explícit

Un fitxer de text és, al capdavall, una seqüència de bytes, i l'encoding és el diccionari que tradueix caràcters ↔ bytes. Amb les lletres ASCII (az) tots els diccionaris coincideixen; el problema són els accents, la ç, l', la ela geminada (l·l)… Si no passes encoding, Python fa servir el de la plataforma: UTF-8 a macOS/Linux, però històricament cp1252 a Windows. El mateix programa que al teu portàtil escriu García pot llegir García (o llançar UnicodeDecodeError) a l'ordinador de la botiga.

# MALAMENT: encoding implicit — depen de la maquina que ho executa
with open("cataleg.txt", "w") as f:
    f.write("Antologia de García Lorca — 14 €\n")

# BE: explicit — el fitxer es identic a Windows, macOS i Linux
with open("cataleg.txt", "w", encoding="utf-8") as f:
    f.write("Antologia de García Lorca — 14 €\n")

Regla del curs, sense excepcions: tot open() de text porta encoding="utf-8", en escriure i en llegir (amb el mateix diccionari en totes dues direccions, és clar). UTF-8 és l'estàndard de facto de la web i de gairebé tot format modern. Una llibreria amb accents i eles geminades a la meitat de les seves fitxes no es pot permetre cap altra cosa.

El salt de línia: \n en escriure, strip() en llegir

El caràcter \n amb què separes les línies en escriure viatja dins de cada línia en llegir: readline(), readlines() i la iteració el conserven al final de la cadena. Si l'oblides, apareixen comparacions que fallen i prints amb línies en blanc fantasma:

with open("cataleg.txt", "r", encoding="utf-8") as f:
    primera = f.readline()

print(repr(primera))                    # 'Cataleg de Papyrus\n'  ← aqui el tens
print(primera == "Cataleg de Papyrus")            # False (per culpa del \n)
print(primera.strip() == "Cataleg de Papyrus")    # True

L'antídot és un vell conegut: strip() (04-05) en processar cada línia llegida — elimina el \n final i, de regal, els espais accidentals. El patró canònic de lectura queda així: for linia in f: + linia.strip() com a primer pas. (Python a més normalitza automàticament els salts de Windows \r\n a \n en mode text, així que strip() també et cobreix aquest cas.)

Promesa saldada: fitxa_llibre.py escriu a registre_altes.txt

Al mòdul 1, fitxa_llibre.py demanava les dades d'un llibre amb input() i mostrava la fitxa amb print() — i vam prometre que algun dia aquesta fitxa no moriria en tancar la finestra. Aquest dia és avui. La versió definitiva dona d'alta el llibre i en deixa constància al disc, fent servir el mode "a" perquè cada execució afegeixi una línia sense esborrar les anteriors:

"""fitxa_llibre.py — alta de llibres amb registre persistent (promesa del modul 1)."""
from dataclasses import dataclass
from datetime import date          # biblioteca estandard, 03-05

NOM_BOTIGA = "Papyrus"
REGISTRE = "registre_altes.txt"

@dataclass
class Llibre:                      # la dataclass canonica de 05-06 (versio minima)
    titol: str
    preu: float
    estoc: int = 0

    def __post_init__(self):
        self.titol = self.titol.strip()
        if not self.titol:
            raise ValueError("El titol no pot estar buit")
        if self.preu < 0:
            raise ValueError(f"Preu negatiu: {self.preu}")

def registrar_alta(llibre):
    """Afegeix una linia al registre d'altes. Mode 'a': mai no esborra l'anterior."""
    with open(REGISTRE, "a", encoding="utf-8") as f:
        f.write(f"{date.today()} | ALTA | {llibre.titol} | {llibre.preu:.2f} | {llibre.estoc}\n")

if __name__ == "__main__":         # 03-04: executable i importable
    titol = input("Titol: ")
    preu = float(input("Preu (€): "))
    estoc = int(input("Estoc: "))
    llibre = Llibre(titol, preu, estoc)
    registrar_alta(llibre)
    print(f"{llibre.titol} donat d'alta a {NOM_BOTIGA} i registrat a {REGISTRE}.")

Després de donar d'alta dos llibres en dies diferents, registre_altes.txt acumula la història:

2026-07-12 | ALTA | Poeta a Nova York | 13.40 | 5
2026-07-13 | ALTA | L'Odissea | 12.50 | 4

Cada peça ve d'un mòdul anterior: input()/f-strings (M1), la dataclass validant en néixer (05-06), date.today() (03-05), i la novetat — with + "a" + encoding="utf-8" — hi posa la persistència. L'Anna ja té una cosa que l'apagada nocturna no pot esborrar.

Errors Comuns i Consells

  • Obrir amb "w" quan volies "a": el fitxer es buida en l'instant de l'open(). Si és un registre històric (altes, vendes), és "a"; "w" és per a abocaments complets que es regeneren sencers.
  • Oblidar encoding="utf-8": funciona a la teva màquina i rebenta (o corromp els accents) en una altra. Explícit sempre, en lectura i en escriptura.
  • Esperar que write() o writelines() afegeixin \n: no ho fan. Tu poses els salts; el nom writelines enganya.
  • Oblidar strip() en llegir: el \n invisible fa fallar comparacions i split(). Primer reflex amb cada línia llegida: linia.strip().
  • Llegir amb read()/readlines() fitxers potencialment grans: ho carreguen tot a la RAM. El mode per defecte és iterar: for linia in f:.
  • Escriure fora del with: després del bloc, el fitxer està tancat; f.write(...) llança ValueError: I/O operation on closed file. Tota la feina amb f, dins del bloc.
  • Oblidar convertir tipus en llegir: del fitxer tot torna com a str. "12.50" + 1 és un TypeError; converteix amb float()/int() just després de fer split().
  • Consell: els fitxers de text s'inspeccionen gratis — obre'ls amb qualsevol editor per verificar què va escriure el teu programa. Aquesta transparència és un gran avantatge respecte dels formats binaris; aprofita-la mentre depures.

Exercicis

Exercici 1: inventari nocturn

Escriu desar_inventari(cataleg, ruta) que rebi el catàleg canònic dict[str, Llibre] (05-06) i escrigui a ruta una línia per llibre amb el format titol | preu | estoc (preu amb 2 decimals), en mode "w" — és l'abocament complet del tancament de caixa, es regenera sencer cada nit. Prova-ho amb els quatre llibres canònics i obre el fitxer amb el teu editor per comprovar-ho.

Exercici 2: recarregar el catàleg en obrir la botiga

Escriu carregar_inventari(ruta) que faci el viatge invers: llegeixi el fitxer de l'exercici 1 línia a línia (iterant, no amb read()), reconstrueixi cada Llibre amb els tipus correctes i retorni el dict[str, Llibre] amb claus normalitzades (normalitzar_titol, 04-05). Si el fitxer no existeix, retorna {} fent servir el pal·liatiu Path.exists() — i deixa-hi un comentari recordant que el mòdul 7 portarà la solució de veritat. Comprova que carregar_inventari("inventari.txt")["hamlet"].preu_final(soci=True) retorna 9.83.

Exercici 3: dietari d'obertura i tancament

Escriu anotar(esdeveniment) que afegeixi a diari.txt (mode "a") una línia data | esdeveniment, amb la data de date.today(). Crida anotar("Obertura de Papyrus") i anotar("Tancament de caixa"), executa l'script dues vegades i comprova que el dietari conserva les quatre línies. Després escriu comptar_esdeveniments(ruta) que retorni un Counter (04-06) d'esdeveniments llegint el dietari línia a línia.

Solucions

Solució 1:

def desar_inventari(cataleg, ruta):
    with open(ruta, "w", encoding="utf-8") as f:          # "w": abocament complet
        for llibre in cataleg.values():
            f.write(f"{llibre.titol} | {llibre.preu:.2f} | {llibre.estoc}\n")

desar_inventari(cataleg, "inventari.txt")

Iterem sobre values() (04-03) perquè la clau normalitzada és reconstruïble; el títol "bonic" viu a l'objecte.

Solució 2:

from pathlib import Path

def carregar_inventari(ruta):
    if not Path(ruta).exists():        # pedac fins al try/except del modul 7
        return {}
    cataleg = {}
    with open(ruta, "r", encoding="utf-8") as f:
        for linia in f:                               # linia a linia: memoria constant
            titol, preu, estoc = linia.strip().split(" | ")
            llibre = Llibre(titol, float(preu), int(estoc))   # str → tipus reals
            cataleg[normalitzar_titol(llibre.titol)] = llibre
    return cataleg

cataleg = carregar_inventari("inventari.txt")
print(cataleg["hamlet"].preu_final(soci=True))    # 9.83 — la xifra canonica

El round-trip complet: els objectes que vas desar ahir a la nit tornen a la vida aquest matí, validats per __post_init__ en reconstruir-se.

Solució 3:

from datetime import date
from collections import Counter

def anotar(esdeveniment):
    with open("diari.txt", "a", encoding="utf-8") as f:
        f.write(f"{date.today()} | {esdeveniment}\n")

def comptar_esdeveniments(ruta):
    esdeveniments = Counter()
    with open(ruta, "r", encoding="utf-8") as f:
        for linia in f:
            _, esdeveniment = linia.strip().split(" | ")
            esdeveniments[esdeveniment] += 1
    return esdeveniments

anotar("Obertura de Papyrus")
anotar("Tancament de caixa")
print(comptar_esdeveniments("diari.txt"))
# despres de dues execucions: Counter({'Obertura de Papyrus': 2, 'Tancament de caixa': 2})

El mode "a" acumula entre execucions — això és exactament la persistència que Papyrus necessitava.

Conclusió

Papyrus ja té memòria a llarg termini: open() amb el mode adequat (r per llegir, w per abocar, a per acumular, x per protegir), sempre dins d'un with que garanteix el tancament, sempre amb encoding="utf-8" perquè els accents i les ces trencades sobrevisquin al viatge, escrivint \n a consciència i netejant amb strip() en tornar. La promesa del mòdul 1 està saldada: fitxa_llibre.py deixa empremta a registre_altes.txt i l'inventari ressuscita cada matí. Però el nostre format titol | preu | estoc és un dialecte casolà: l'hem inventat nosaltres, ningú més no l'entén, i es trenca el dia que un títol contingui una barra vertical. Existeix un format de text tabular que Excel, Google Sheets i mig món parlen de naixement, amb un mòdul de la biblioteca estàndard que et resol separadors, cometes i casos retorçats: CSV. A la propera lliçó, el catàleg i les vendes de Papyrus s'hi muden.

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