CSV va deixar Papyrus parlant l'idioma de les taules, però amb un sostre a la vista: les taules són planes. Els socis de la botiga no ho són — en Lluís (LLUIS-001) té un nom, un codi i una llista de compres, cadascuna amb la seva data i la seva quantia. Encabir una llista dins d'una cel·la de CSV és forçar el format; per a estructures niades existeix JSON (JavaScript Object Notation), el format de text en què avui s'escriuen les configuracions de les aplicacions i les respostes de gairebé totes les APIs web. En aquesta lliçó aprendràs les quatre funcions del mòdul json, la taula d'equivalències entre tipus Python i JSON (i què en queda fora), i portaràs el fil de Papyrus dos passos més enllà: el catàleg de dataclasses viatjarà a JSON amb asdict() i tornarà amb Llibre(**dades), i la botiga estrenarà el seu config.json.

Contingut

  1. Què és JSON i on viu
  2. Les quatre funcions del mòdul json: dump, load, dumps, loads
  3. Tipus Python ↔ JSON: la taula d'equivalències
  4. indent i ensure_ascii=False: JSON llegible i amb accents
  5. El catàleg en JSON: asdict() a l'anada, Llibre(**dades) a la tornada
  6. Niament: els socis de Papyrus amb les seves compres
  7. JSON vs CSV: taula de criteri
  8. config.json: les constants de Papyrus surten del codi
  9. Una menció a pickle
  10. Errors comuns i consells
  11. Exercicis amb solucions

Què és JSON i on viu

JSON és un format de text pla per representar dades estructurades. La seva sintaxi et resultarà sospitosament familiar:

{
    "titol": "L'Odissea",
    "preu": 12.5,
    "estoc": 4,
    "disponible": true,
    "etiquetes": ["classics", "poesia epica"]
}

Sembla un diccionari de Python amb una llista a dins — i gairebé ho és (les diferències, a la taula d'equivalències). Va néixer al món JavaScript, però avui és neutral i omnipresent. El trobaràs en tres hàbitats:

  • APIs web: quan al mòdul 10 el teu programa demani dades a un servidor (o les serveixi amb Flask), les respostes seran JSON.
  • Fitxers de configuració: preferències d'aplicacions, ajustos d'editors (el settings.json de VS Code, sense anar més lluny).
  • Intercanvi i emmagatzematge de dades estructurades: just el que Papyrus necessita per al catàleg i els socis.

A diferència de CSV, JSON nia sense límit (objectes dins de llistes dins d'objectes) i conserva els tipus bàsics: un 12.5 torna com a número, no com la cadena "12.5". El peatge de conversió que vam pagar a 06-01 i 06-02 gairebé desapareix.

Les quatre funcions del mòdul json: dump, load, dumps, loads

Tota la lliçó gira sobre quatre funcions de la biblioteca estàndard. El truc mnemotècnic: la s final significa string — les versions amb s treballen amb cadenes en memòria; les versions sense s, amb fitxers oberts.

Funció Direcció Treballa amb Ús típic
json.dump(dades, f) Python → JSON Fitxer obert per escriure Desar el catàleg a cataleg.json
json.load(f) JSON → Python Fitxer obert per llegir Carregar config.json en arrencar
json.dumps(dades) Python → JSON Retorna una cadena Enviar dades a una API, imprimir per depurar
json.loads(cadena) JSON → Python Rep una cadena Interpretar la resposta d'una API

Les quatre en acció:

import json

llibre = {"titol": "Hamlet", "preu": 9.95, "estoc": 6}

# dumps/loads: cadena en memoria (fixa-t'hi: sense tocar el disc)
text = json.dumps(llibre)
print(text)                        # {"titol": "Hamlet", "preu": 9.95, "estoc": 6}
print(type(text))                  # <class 'str'>
copia = json.loads(text)
print(copia["preu"] + 1)           # 10.95 — torna com a float, no com a "9.95"!

# dump/load: directe a fitxer — les regles de 06-01 segueixen vigents (with + utf-8)
with open("llibre.json", "w", encoding="utf-8") as f:
    json.dump(llibre, f)

with open("llibre.json", "r", encoding="utf-8") as f:
    recuperat = json.load(f)
print(recuperat == llibre)         # True — round-trip perfecte, tipus inclosos

Aquest copia["preu"] + 1 marca la diferència amb tot l'anterior: en text pla i CSV, els números tornaven com a cadenes i convertíes a mà; JSON recorda que 9.95 era un número. (El newline="" de 06-02 era una peculiaritat del mòdul csv; amb json no es fa servir.)

Tipus Python ↔ JSON: la taula d'equivalències

La traducció entre tots dos mons està definida tipus a tipus:

Python (en escriure) JSON Python (en llegir de tornada)
dict objecte {...} dict
list, tuple array [...] list (la tupla no torna com a tupla!)
str string "..." str
int número int
float número float
True / False true / false True / False
None null None

I la lletra petita del contracte, que convé llegir:

  • Les claus d'un objecte JSON són sempre cadenes. Un dict de Python amb claus enteres ({1: "a"}) s'escriu com {"1": "a"} i en tornar les claus són str. Si fas servir claus no textuals, el round-trip no és fidel.
  • Les tuples es degraden a llistes: ("a", "b")["a", "b"]["a", "b"]. JSON no té tuples.
  • El que NO se serialitza: set, datetime/date, bytes i — atenció — els teus objectes: json.dumps(Llibre("Hamlet", 9.95, 6)) llança TypeError: Object of type Llibre is not JSON serializable. L'estratègia estàndard és convertir abans a tipus bàsics (un setlist, un date"2026-07-13" amb str() o isoformat(), i una dataclass → dict, com veuràs d'aquí a dues seccions).

Aquesta darrera limitació no és un defecte: JSON és deliberadament petit perquè qualsevol llenguatge pugui llegir-lo. Els teus objectes són de Python; les seves dades, universals.

indent i ensure_ascii=False: JSON llegible i amb accents

Per defecte, dump/dumps ho escriuen tot en una línia i escapen els caràcters no ASCII. El resultat funciona, però és il·legible per a humans:

config = {"nom_botiga": "Papyrus", "especialitat": "clàssics"}
print(json.dumps(config))
# {"nom_botiga": "Papyrus", "especialitat": "cl\u00e0ssics"}   ← i els meus accents?

Dos arguments ho arreglen:

  • indent=4: formata amb salts de línia i sagnat de 4 espais — imprescindible en fitxers que una persona hagi de llegir o editar (configuracions).
  • ensure_ascii=False: escriu à en lloc de \u00e0. L'escapament és una protecció arcaica per a sistemes que no entenen UTF-8; com que nosaltres obrim sempre amb encoding="utf-8" (06-01), podem desactivar-la i tenir accents de veritat al fitxer.
print(json.dumps(config, indent=4, ensure_ascii=False))
{
    "nom_botiga": "Papyrus",
    "especialitat": "clàssics"
}

Regla pràctica del curs: en fitxers JSON destinats a humans, indent=4, ensure_ascii=False sempre. En JSON que viatja entre programes (APIs), la versió compacta estalvia bytes i ningú no la llegeix.

El catàleg en JSON: asdict() a l'anada, Llibre(**dades) a la tornada

L'obstacle — "els teus objectes no se serialitzen" — té una solució elegant per a dataclasses, i ve a la mateixa biblioteca: dataclasses.asdict() converteix una dataclass (recursivament, camps niats inclosos) en un diccionari de tipus bàsics, que JSON sí que entén:

from dataclasses import asdict

odissea = Llibre("L'Odissea", 12.50, 4)   # la dataclass canonica de 05-06
print(asdict(odissea))                    # {'titol': "L'Odissea", 'preu': 12.5, 'estoc': 4}

I per al viatge de tornada, el desempaquetament de diccionaris amb ** (03-02): Llibre(**{"titol": "L'Odissea", "preu": 12.5, "estoc": 4}) equival a Llibre(titol="L'Odissea", preu=12.5, estoc=4). Com que els camps de la dataclass es diuen igual que les claus del dict, encaixen sols. El round-trip complet del catàleg queda així:

import json
from pathlib import Path
from dataclasses import asdict

def desar_cataleg_json(cataleg, ruta="cataleg.json"):
    """dict[str, Llibre] → llista de dicts → JSON."""
    dades = [asdict(llibre) for llibre in cataleg.values()]
    with open(ruta, "w", encoding="utf-8") as f:
        json.dump(dades, f, indent=4, ensure_ascii=False)

def carregar_cataleg_json(ruta="cataleg.json"):
    """JSON → llista de dicts → dict[str, Llibre]. Sense conversions str→float: JSON recorda."""
    if not Path(ruta).exists():          # el pedac de sempre; el modul 7 arriba al rescat aviat
        return {}
    with open(ruta, "r", encoding="utf-8") as f:
        dades = json.load(f)
    return {normalitzar_titol(d["titol"]): Llibre(**d) for d in dades}

desar_cataleg_json(cataleg)
recarregat = carregar_cataleg_json()
print(recarregat == cataleg)                          # True
print(recarregat["hamlet"].preu_final(soci=True))     # 9.83 — la xifra de sempre

Compara carregar_cataleg_json amb el carregar_cataleg de 06-02: han desaparegut float(fila["preu"]) i int(fila["estoc"]). JSON va retornar cada tipus intacte i Llibre(**d) va reconstruir l'objecte en una expressió — amb __post_init__ validant de regal, com sempre. El cataleg.json resultant és una llista d'objectes:

[
    {
        "titol": "L'Odissea",
        "preu": 12.5,
        "estoc": 4
    },
    {
        "titol": "Hamlet",
        "preu": 9.95,
        "estoc": 6
    }
]

(Nota al marge: desem cataleg.values() com a llista i reconstruïm les claus normalitzades en carregar, igual que a 06-02 — la clau és derivable, no cal emmagatzemar-la.)

Niament: els socis de Papyrus amb les seves compres

Aquí JSON juga en una lliga on CSV no arriba. Cada soci és un objecte amb una llista de compres a dins, i cada compra és al seu torn un objecte — l'estructura calca les LiniaVenda de 05-06:

socis = [
    {
        "nom": "Lluis", "codi": "LLUIS-001",
        "compres": [
            {"data": "2026-07-13", "titol": "Hamlet", "quantia": 9.83},
            {"data": "2026-07-11", "titol": "L'Odissea", "quantia": 12.35},
        ],
    },
    {
        "nom": "Marta", "codi": "MARTA-002",
        "compres": [
            {"data": "2026-07-12", "titol": "El Quixot", "quantia": 15.71},
        ],
    },
    {"nom": "Pau", "codi": "PAU-003", "compres": []},
]

with open("socis.json", "w", encoding="utf-8") as f:
    json.dump(socis, f, indent=4, ensure_ascii=False)

I en carregar, navegues per l'estructura amb la sintaxi de sempre — índexs i claus encadenats (04-01/04-03), o millor, bucles i comprensions:

with open("socis.json", "r", encoding="utf-8") as f:
    socis = json.load(f)

print(socis[0]["compres"][1]["titol"])         # L'Odissea

despesa_per_soci = {s["codi"]: round(sum(c["quantia"] for c in s["compres"]), 2)
                    for s in socis}
print(despesa_per_soci)   # {'LLUIS-001': 22.18, 'MARTA-002': 15.71, 'PAU-003': 0}

Intenta imaginar aquesta dada en CSV: o una fila per compra repetint les dades del soci, o llistes encabides en una cel·la. JSON ho expressa tal com ho penses.

JSON vs CSV: taula de criteri

Dues lliçons, dos formats de text. El criteri per triar:

CSV (06-02) JSON (aquesta lliçó)
Forma de les dades Taula plana: files × columnes Arbre: niament lliure
Tipus Tot torna com a str Conserva número/booleà/null
L'obre Excel/Sheets Sí, directament No (és per a programes)
Mida amb moltes files Compacte (capçalera una vegada) Verbós (claus repetides per objecte)
Hàbitat natural Exportacions, fulls de càlcul, dades massives per files APIs, configuracions, estructures niades
A Papyrus vendes.csv (històric tabular que l'Anna obre a Excel) config.json, socis.json (estructura i tipus)

Regla ràpida: les dades són una taula que un humà voldrà obrir en un full de càlcul? CSV. Tenen estructura, niament o tipus que importen? JSON. El catàleg de Papyrus, com que és tabular, viu còmode en tots dos — l'hem desat en els dos formats precisament perquè comparis el codi.

config.json: les constants de Papyrus surten del codi

Des del mòdul 1, NOM_BOTIGA, IVA_LLIBRES i DESCOMPTE_SOCI viuen escrites al codi. Funciona, però canviar el descompte de soci obliga a editar un .py. La manera professional és externalitzar la configuració a un fitxer que un no-programador pugui tocar — i JSON amb indent és ideal:

{
    "nom_botiga": "Papyrus",
    "iva_llibres": 0.04,
    "descompte_soci": 0.05
}
import json

def carregar_config(ruta="config.json"):
    with open(ruta, "r", encoding="utf-8") as f:
        return json.load(f)

config = carregar_config()
NOM_BOTIGA = config["nom_botiga"]            # "Papyrus"
IVA_LLIBRES = config["iva_llibres"]          # 0.04 — float de debo, a punt per multiplicar
DESCOMPTE_SOCI = config["descompte_soci"]    # 0.05

Si demà el gremi pacta un altre descompte, l'Anna edita config.json amb el Bloc de notes i cap .py no canvia. Observa que els tipus arriben correctes sense conversió: 0.04 és float perquè JSON ho recorda. (I si config.json no existeix o algú el trenca editant-lo? json.load llança JSONDecodeError — un altre client esperant a la porta del mòdul 7.)

Una menció a pickle

La biblioteca estàndard inclou una altra via de persistència: pickle, que serialitza objectes Python tal qual — sets, dates, les teves classes, gairebé qualsevol cosa — a un format binari (fitxers en mode "wb"/"rb", els que 06-01 va deixar apuntats):

import pickle

with open("cataleg.pkl", "wb") as f:       # binari: sense encoding
    pickle.dump(cataleg, f)                # el dict[str, Llibre] sencer, objectes inclosos

with open("cataleg.pkl", "rb") as f:
    recarregat = pickle.load(f)            # tornen objectes Llibre, sense asdict ni **

Temptador, però té tres lletres petites que el releguen a usos puntuals: és només-Python (cap altre llenguatge ni Excel no el llegirà), és il·legible (no pots obrir-lo en un editor per inspeccionar-lo, el gran avantatge del text que venim explotant) i és insegur amb dades alienes — despicklejar un fitxer d'origen no fiable pot executar codi arbitrari; la mateixa documentació oficial ho adverteix. Criteri del curs: per desar estat propi i temporal entre execucions teves, val; per intercanviar, configurar o arxivar, text (CSV/JSON) sempre. Papyrus es queda amb JSON.

Errors Comuns i Consells

  • Confondre les quatre funcions: json.dump(dades, "cataleg.json") falla — dump vol un fitxer obert, no una ruta. I json.load(text) també — per a cadenes és loads. Mnemotècnia: la s és de string.
  • Intentar serialitzar objectes directament: json.dump(cataleg, f) amb valors Llibre llança TypeError: ... not JSON serializable. Primer asdict() (o construeix dicts/llistes a mà).
  • Oblidar que les tuples i els sets no sobreviuen: la tupla torna com a llista i el set ni tan sols s'escriu (TypeError). Converteix el set a list abans i reconverteix-lo en carregar si t'importa la deduplicació.
  • Claus no textuals: {2026: "any"} es desa com {"2026": "any"} i en tornar la clau és str. Si la clau és un número que importa, desa'l com a valor.
  • Editar JSON a mà i trencar-lo: una coma final després de l'últim element ("estoc": 4,}) o cometes simples són JSON invàlid → JSONDecodeError en carregar. Els editors moderns ho marquen; fins al mòdul 7, revisa abans de desar.
  • Escriure sense ensure_ascii=False en fitxers que llegiran persones: els accents es converteixen en seqüències \u00e0 i el fitxer sembla corromput sense estar-ho.
  • Consell: per inspeccionar un JSON compacte rebut de fora, print(json.dumps(dades, indent=4, ensure_ascii=False)) l'"embelleix" a l'instant. És el pretty-print de depuració estàndard.

Exercicis

Exercici 1: l'estat complet de la botiga

Escriu desar_estat(cataleg, ruta="estat.json") que desi en un únic JSON un objecte amb tres claus: "botiga" (el nom, des de la constant), "desat" (la data d'avui com a cadena str(date.today()) — recorda que date no se serialitza) i "cataleg" (la llista d'asdict()). Escriu també carregar_estat(ruta) que retorni la tupla (data, dict[str, Llibre]). Comprova el round-trip amb els quatre llibres canònics.

Exercici 2: la compra queda a l'historial

Escriu registrar_compra_soci(codi, compra, ruta="socis.json") que carregui el JSON de socis, localitzi el soci pel seu camp "codi", afegeixi el diccionari compra a la seva llista "compres" i reescrigui el fitxer complet (patró carregar → modificar → desar). Registra a en Pau (PAU-003) la compra {"data": "2026-07-13", "titol": "Faust", "quantia": 20.75} — per fi van arribar exemplars — i comprova rellegint el fitxer que la seva llista ja no és buida. Si el codi no existeix, imprimeix un avís i no toquis el fitxer.

Exercici 3: criteri de format

Per a cada dada de Papyrus, decideix CSV o JSON i justifica-ho en un comentari d'una línia: (a) l'històric de deu anys de vendes que l'Anna filtra a Excel cada trimestre; (b) les preferències de l'aplicació de caixa (nom, IVA, descompte, imprimir tiquet?); (c) els socis amb les seves llistes de compres; (d) la llista de novetats que la distribuïdora importa al seu sistema, "en el que sigui, però que ho obri tothom". Després escriu la fitxa del soci Lluís (amb dues compres) en tots dos formats i observa quin força l'estructura.

Solucions

Solució 1:

import json
from datetime import date
from dataclasses import asdict
from pathlib import Path

def desar_estat(cataleg, ruta="estat.json"):
    estat = {
        "botiga": NOM_BOTIGA,
        "desat": str(date.today()),               # date → str: el peatge del no serialitzable
        "cataleg": [asdict(l) for l in cataleg.values()],
    }
    with open(ruta, "w", encoding="utf-8") as f:
        json.dump(estat, f, indent=4, ensure_ascii=False)

def carregar_estat(ruta="estat.json"):
    if not Path(ruta).exists():
        return None, {}
    with open(ruta, "r", encoding="utf-8") as f:
        estat = json.load(f)
    cataleg = {normalitzar_titol(d["titol"]): Llibre(**d) for d in estat["cataleg"]}
    return estat["desat"], cataleg

desar_estat(cataleg)
data, recarregat = carregar_estat()
print(data, recarregat == cataleg)       # 2026-07-13 True

Un sol fitxer amb metadades + dades: l'embrió del "desar partida" de qualsevol aplicació.

Solució 2:

import json

def registrar_compra_soci(codi, compra, ruta="socis.json"):
    with open(ruta, "r", encoding="utf-8") as f:
        socis = json.load(f)

    for soci in socis:
        if soci["codi"] == codi:
            soci["compres"].append(compra)        # modificar en memoria...
            break
    else:                                         # el for/else de 02-02: no s'ha trobat
        print(f"AVIS: no existeix el soci {codi}")
        return

    with open(ruta, "w", encoding="utf-8") as f:  # ...i reescriure sencer
        json.dump(socis, f, indent=4, ensure_ascii=False)

registrar_compra_soci("PAU-003", {"data": "2026-07-13", "titol": "Faust", "quantia": 20.75})

JSON no es pot "apendre" com el CSV de vendes: el fitxer és una estructura completa, així que el patró és sempre carregar → modificar → abocar. (20.75 és la xifra canònica: el preu de soci de Faust.)

Solució 3:

# (a) CSV   — tabular, massiu i desti Excel: el seu habitat exacte
# (b) JSON  — tipus barrejats (str, float, bool) i estructura de configuracio
# (c) JSON  — llistes niades dins de cada soci; en CSV no hi caben be
# (d) CSV   — "que ho obri tothom" es la definicio de lingua franca

En escriure la fitxa d'en Lluís en tots dos: en JSON les compres nien amb naturalitat; en CSV acabes repetint Lluis,LLUIS-001 a cada fila de compra o inventant un subformat dins d'una cel·la — el senyal inequívoc que el format no encaixa.

Conclusió

JSON completa l'arsenal de persistència de Papyrus: dump/load per a fitxers i dumps/loads per a cadenes, tipus que sobreviuen al viatge (adeu al peatge strfloat), indent=4, ensure_ascii=False per a fitxers llegibles per humans, asdict() + Llibre(**dades) com a pont idiomàtic entre dataclasses i disc, niament per als socis amb les seves compres i un config.json que treu les constants del codi. CSV per a taules amb destí Excel, JSON per a estructura i configuració, pickle només per a estat propi i amb cautela. Però fixa't en el que se'ns està acumulant: registre_altes.txt, cataleg.csv, vendes.csv, cataleg.json, socis.json, config.json… mitja dotzena de fitxers escampats a la mateixa carpeta que el codi. Una botiga endreçada no amuntega les caixes al costat de la caixa registradora: calen carpetes (dades/, copies/, informes/), rutes que funcionin en qualsevol màquina i còpies de seguretat nocturnes amb data. D'organitzar tot això — amb pathlib com a protagonista i el mòdul os que el mòdul 3 va deixar promès — se n'ocupa l'última lliçó del mòdul.

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