JSON : lire, écrire et enregistrer des données

Objectifs de la leçon
  • Expliquer ce qu’est JSON et pourquoi il vaut mieux qu’un fichier texte « libre » pour enregistrer des données
  • Connaître la correspondance entre les types JSON et les types Python
  • Enregistrer et charger des données depuis un fichier avec json.dump et json.load
  • Convertir entre une chaîne JSON et un objet Python avec json.dumps et json.loads, et gérer un fichier manquant ou corrompu

Qu’allons-nous apprendre ?

  • Ce qu’est JSON et à quoi il ressemble.
  • Comment JSON correspond aux dict et list de Python.
  • Enregistrer dans un fichier et charger depuis un fichier.
  • Convertir entre une chaîne JSON et des données.

Pourquoi en avons-nous besoin ?

Dans la leçon précédente, nous avons enregistré du texte, et pour cela nous avons inventé notre propre « format » : Dana,90 sur chaque ligne. Supposons maintenant que chaque étudiant ait une liste de notes, une adresse avec une ville et une rue, et un indicateur is_active. Comment enregistrer cela dans une ligne de texte ? Chacun inventera un format différent, et il faudra écrire un code d’analyse compliqué.

JSON est un format standard pour les données structurées. Tous les langages de programmation savent le lire et l’écrire. Et surtout :

  • Le thème POO — le projet sera enregistré en JSON.
  • Les thèmes backend et bases de données — chaque requête et chaque réponse d’une API est envoyée en JSON. FastAPI convertit automatiquement un dict Python en JSON et inversement.

Explication à partir des bases

Qu’est-ce que JSON ?

JSON = JavaScript Object Notation. Il est né dans JavaScript, mais c’est aujourd’hui un format général. Un fichier JSON est un fichier texte à la structure définie, qui ressemble presque exactement aux dict et list de Python :

{
  "name": "Dana",
  "age": 24,
  "is_active": true,
  "grades": [90, 85, 100],
  "address": {"city": "Haifa", "street": "Herzl 10"},
  "phone": null
}

La correspondance entre JSON et Python

JSONPython
object { }dict
array [ ]list
string "text"str
number 42, 3.5int, float
true / falseTrue / False
nullNone

Les règles importantes de JSON (différentes de Python !)

  • Les chaînes et les clés uniquement entre guillemets doubles "...". Les guillemets simples '...' ne sont pas valides.
  • true, false, null en minuscules.
  • Pas de virgule après le dernier élément.
  • Pas de commentaires.
  • Les clés d’un objet sont toujours des chaînes.

La sérialisation et la désérialisation

TermeSensFonction
Sérialisation (Serialization)Données Python → texte JSONjson.dump / json.dumps
Désérialisation (Deserialization)Texte JSON → données Pythonjson.load / json.loads

Comment s’en souvenir : le s à la fin (dumps, loads) = string, on travaille avec une chaîne. Sans s (dump, load) = on travaille avec un fichier.

Analogie : emballer des meubles pour un déménagement

Un canapé dans le salon (un dict en mémoire) ne passe pas la porte tel quel. La sérialisation = le démonter et l’emballer dans un carton plat avec une notice de montage (le texte JSON), que l’on peut garder dans une réserve (un fichier) ou envoyer dans un camion (le réseau). La désérialisation = le remonter dans la nouvelle maison, et obtenir un canapé identique.

Syntaxe

import json

data = {"name": "Dana", "grades": [90, 85]}

# file
with open(path, "w", encoding="utf-8") as file:
    json.dump(data, file, indent=2, ensure_ascii=False)   # save

with open(path, "r", encoding="utf-8") as file:
    data = json.load(file)                                # load

# string
text = json.dumps(data)        # dict -> JSON string
data = json.loads(text)        # JSON string -> dict
  • indent=2 — écrit un JSON « joli », avec des sauts de ligne et une indentation. Sans lui, tout est sur une seule ligne.
  • ensure_ascii=False — un texte qui n’est pas en anglais (hébreu, caractères accentués, par exemple) est enregistré tel quel, et non sous forme de codes d’échappement (une barre oblique inverse, u et quatre chiffres hexadécimaux pour chaque lettre).

Premier exemple de code

week-2/files/save_profile.py :

import json
from pathlib import Path

PROFILE_FILE = Path(__file__).parent / "profile.json"

profile = {
    "name": "Dana",
    "age": 24,
    "is_active": True,
    "grades": [90, 85, 100],
    "address": {"city": "Haifa", "street": "Herzl 10"},
    "phone": None,
}

with open(PROFILE_FILE, "w", encoding="utf-8") as file:
    json.dump(profile, file, indent=2, ensure_ascii=False)

with open(PROFILE_FILE, "r", encoding="utf-8") as file:
    loaded = json.load(file)

print("Loaded type:", type(loaded))
print("City:", loaded["address"]["city"])
print("Average:", sum(loaded["grades"]) / len(loaded["grades"]))
print("Same as original?", loaded == profile)

Sortie :

Loaded type: <class 'dict'>
City: Haifa
Average: 91.66666666666667
Same as original? True

Le profile.json qui a été créé :

{
  "name": "Dana",
  "age": 24,
  "is_active": true,
  "grades": [
    90,
    85,
    100
  ],
  "address": {
    "city": "Haifa",
    "street": "Herzl 10"
  },
  "phone": null
}

Analyse du code ligne par ligne

LigneExplication
import jsonUn module standard. Aucune installation nécessaire.
json.dump(profile, file, indent=2, ...)Sérialisation : transforme le dict en texte JSON et l’écrit dans le fichier. True est devenu true, et None est devenu null.
json.load(file)Désérialisation : lit le texte et construit un nouveau dict à partir de lui.
type(loaded)Un dict tout à fait ordinaire. On peut travailler avec lui comme avec n’importe quel dict.
loaded["address"]["city"]Accès à une structure imbriquée, comme dans le thème structures de données et fonctions.
loaded == profileTrue : les données ont survécu intégralement à « l’aller-retour » vers le fichier. (Mais loaded is profile vaut False, car c’est un nouvel objet.)

Un autre exemple

Une petite « base de données » dans un fichier JSON — chargement avec une valeur par défaut, ajout, enregistrement et gestion d’un fichier corrompu. Exactement le modèle qui servira dans le projet de POO. week-2/files/contacts_db.py :

import json
import logging
from pathlib import Path

logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")

DB_FILE = Path(__file__).parent / "contacts.json"

def load_contacts():
    if not DB_FILE.exists():
        logging.info("No database file yet - starting empty")
        return []
    try:
        with open(DB_FILE, "r", encoding="utf-8") as file:
            return json.load(file)
    except json.JSONDecodeError:
        logging.error("Database file is corrupted - starting empty")
        return []

def save_contacts(contacts):
    with open(DB_FILE, "w", encoding="utf-8") as file:
        json.dump(contacts, file, indent=2, ensure_ascii=False)
    logging.info("Saved %s contacts", len(contacts))

def add_contact(contacts, name, phone):
    contact = {"id": len(contacts) + 1, "name": name, "phone": phone}
    contacts.append(contact)
    return contact

def main():
    contacts = load_contacts()
    new_contact = add_contact(contacts, "Dana", "050-1234567")
    print("Added:", new_contact)
    save_contacts(contacts)
    print("Total contacts in file:", len(contacts))

if __name__ == "__main__":
    main()

Première exécution :

INFO No database file yet - starting empty
Added: {'id': 1, 'name': 'Dana', 'phone': '050-1234567'}
INFO Saved 1 contacts
Total contacts in file: 1

Deuxième exécution :

Added: {'id': 2, 'name': 'Dana', 'phone': '050-1234567'}
INFO Saved 2 contacts
Total contacts in file: 2

Analyse :

  • Le fichier contient une liste de dicts : exactement un « tableau » d’enregistrements.
  • load_contacts gère deux cas : pas de fichier (la première exécution) → une liste vide. Un fichier corrompu (json.JSONDecodeError) → une erreur journalisée et une liste vide. Dans les deux cas, le programme ne plante pas.
  • "id": len(contacts) + 1 — un identifiant unique simple. (Remarquez : si un contact est supprimé, l’identifiant suivant risque de répéter un identifiant existant. Dans une vraie base de données, dans les thèmes backend et bases de données, c’est la base elle-même qui attribuera les identifiants, et c’est justement l’une des raisons de l’utiliser.)
  • Charger → modifier en mémoire → enregistrer : le modèle de base du travail avec un fichier de données.

Une chaîne JSON — dumps / loads

Il n’y a pas toujours de fichier. Sur le réseau, par exemple, on reçoit une chaîne JSON :

import json

response_text = '{"id": 7, "name": "Dana", "active": true, "tags": ["new", "vip"]}'

student = json.loads(response_text)
print(student["name"], student["active"], student["tags"][1])

reply = {"status": "ok", "count": 3, "error": None}
print(json.dumps(reply))
Dana True vip
{"status": "ok", "count": 3, "error": null}

C’est exactement ce que fera FastAPI pour nous dans les thèmes backend et bases de données : loads sur la requête entrante, et dumps sur la réponse sortante.

Erreur courante

Erreur 1 : un JSON invalide écrit à la main.

json.loads("{'name': 'Dana'}")
json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)

En JSON, uniquement des guillemets doubles. La raison courante : copier la sortie de print(dict) de Python, qui utilise des guillemets simples, ce n’est donc pas du JSON valide. La bonne façon de créer du JSON est toujours json.dumps, et non str(dict).

Erreur 2 : confondre dump et dumps.

with open(path, "w", encoding="utf-8") as file:
    json.dumps(data, file)
TypeError: dumps() takes 1 positional argument but 2 were given

Pour un fichier, utilisez dump (sans s). dumps renvoie une chaîne, et n’écrit nulle part.

Erreur 3 : enregistrer quelque chose que JSON ne connaît pas.

json.dumps({"tags": {"a", "b"}})
TypeError: Object of type set is not JSON serializable

JSON n’a pas de set (ni de tuple : un tuple est enregistré comme un array et rechargé sous forme de liste). Convertissez d’abord : list(my_set). La même chose arrivera dans le thème POO avec les objets des classes, et nous y apprendrons à les convertir en dict.

Erreur 4 : les clés numériques reviennent sous forme de chaînes.

data = {1: "one"}
loaded = json.loads(json.dumps(data))
print(loaded)        # {'1': 'one'}
print(loaded[1])     # KeyError: 1

En JSON, les clés sont toujours des chaînes, donc 1 est devenu "1". Accédez-y avec loaded["1"], ou prévoyez dès le départ des clés textuelles.

Exercices

Les exercices n’utilisent que ce qui a été vu dans cette leçon et les précédentes. Essayez d’abord seul, et n’ouvrez la solution qu’ensuite.

Exercice 1: De Python à JSON

Pour chaque valeur Python — à quoi ressemblera-t-elle en JSON ?

True        None        "Dana"        [1, 2]        {"age": 24}
Afficher la solution
PythonJSON
Truetrue
Nonenull
"Dana""Dana" — toujours des guillemets doubles
[1, 2][1, 2] — un array
{"age": 24}{"age": 24} — un objet ; les clés sont toujours des chaînes

Exercice 2: Enregistrer et charger

Enregistrez le dictionnaire dans le fichier profile.json (avec une indentation, et sans encoder le texte non anglais), rechargez-le, et affichez le nom et la note la plus haute.

profile = {"name": "Dana", "grades": [90, 85, 100]}

Résultat attendu :

Dana
100
Afficher la solution
import json

profile = {"name": "Dana", "grades": [90, 85, 100]}

with open("profile.json", "w", encoding="utf-8") as file:
    json.dump(profile, file, indent=2, ensure_ascii=False)

with open("profile.json", "r", encoding="utf-8") as file:
    loaded = json.load(file)

print(loaded["name"])
print(max(loaded["grades"]))
Dana
100
  • dump / load — avec un fichier.
  • indent=2 — un fichier lisible par des humains. ensure_ascii=False — le texte non anglais est enregistré tel quel.

Exercice 3: Une chaîne JSON

La réponse suivante est arrivée d’un serveur sous forme de chaîne. Convertissez-la en dictionnaire et affichez le nom et si l’étudiant est actif. Convertissez ensuite le dictionnaire reply en chaîne JSON.

text = '{"id": 7, "name": "Dana", "active": true}'
reply = {"status": "ok", "error": None}

Résultat attendu :

Dana True
{"status": "ok", "error": null}
Afficher la solution
import json

text = '{"id": 7, "name": "Dana", "active": true}'
student = json.loads(text)
print(student["name"], student["active"])

reply = {"status": "ok", "error": None}
print(json.dumps(reply))
Dana True
{"status": "ok", "error": null}

loads / dumps (avec un s — string) travaillent avec une chaîne, pas avec un fichier. C’est à cela que ressemble la communication avec un serveur.

Exercice 4: Des réglages qui sont enregistrés

Écrivez load_settings(path) qui renvoie les valeurs par défaut {"theme": "light", "font_size": 14}, et si le fichier existe — les met à jour selon ce qui y est enregistré. Écrivez aussi save_settings(path, settings).

print(load_settings("settings.json"))
save_settings("settings.json", {"theme": "dark"})
print(load_settings("settings.json"))

Résultat attendu :

{'theme': 'light', 'font_size': 14}
{'theme': 'dark', 'font_size': 14}
Afficher la solution
import json
from pathlib import Path

DEFAULTS = {"theme": "light", "font_size": 14}


def load_settings(path):
    settings = DEFAULTS.copy()
    if Path(path).exists():
        with open(path, "r", encoding="utf-8") as file:
            saved = json.load(file)
        for key, value in saved.items():
            settings[key] = value
    return settings


def save_settings(path, settings):
    with open(path, "w", encoding="utf-8") as file:
        json.dump(settings, file, indent=2)


print(load_settings("settings.json"))
save_settings("settings.json", {"theme": "dark"})
print(load_settings("settings.json"))
{'theme': 'light', 'font_size': 14}
{'theme': 'dark', 'font_size': 14}

DEFAULTS.copy() — pour ne pas modifier les valeurs par défaut elles-mêmes (vous vous souvenez des références de la leçon sur la mutabilité ?).

Exercice 5: Une liste de contacts

Écrivez deux fonctions : add_contact(path, name, phone) qui ajoute un contact à une liste enregistrée dans un fichier JSON (et crée le fichier s’il n’existe pas), et find_phone(path, name) qui renvoie le téléphone ou None.

add_contact("contacts.json", "Dana", "050-1111111")
add_contact("contacts.json", "Avi", "052-2222222")
print(find_phone("contacts.json", "Avi"))
print(find_phone("contacts.json", "Noa"))

Résultat attendu :

052-2222222
None
Afficher la solution
import json
from pathlib import Path


def load_contacts(path):
    if not Path(path).exists():
        return []
    with open(path, "r", encoding="utf-8") as file:
        return json.load(file)


def add_contact(path, name, phone):
    contacts = load_contacts(path)
    contacts.append({"name": name, "phone": phone})
    with open(path, "w", encoding="utf-8") as file:
        json.dump(contacts, file, indent=2, ensure_ascii=False)


def find_phone(path, name):
    for contact in load_contacts(path):
        if contact["name"] == name:
            return contact["phone"]
    return None


add_contact("contacts.json", "Dana", "050-1111111")
add_contact("contacts.json", "Avi", "052-2222222")
print(find_phone("contacts.json", "Avi"))
print(find_phone("contacts.json", "Noa"))
052-2222222
None

Le fichier contient une liste de dictionnaires — exactement la structure de la leçon sur dict. À chaque modification : charger, modifier, enregistrer.

Résumé

  • JSON = un format texte standard pour les données structurées. object ↔ dict, array ↔ list, true/false/null ↔ True/False/None.
  • Règles : guillemets doubles uniquement, pas de virgule finale, pas de commentaires. Les clés sont toujours des chaînes.
  • Fichier : json.dump(data, file) / json.load(file). Chaîne : json.dumps(data) / json.loads(text).
  • indent=2 pour la lisibilité, ensure_ascii=False pour un texte non anglais.
  • Le modèle : charger (avec une valeur par défaut) → modifier en mémoire → enregistrer. Gérez un fichier manquant et JSONDecodeError.
  • Les sets et les objets doivent être convertis avant l’enregistrement. JSON est la langue de toute API dans les thèmes backend et bases de données.

Documentation officielle