JSON : lire, écrire et enregistrer des données
- 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.dumpetjson.load - Convertir entre une chaîne JSON et un objet Python avec
json.dumpsetjson.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
| JSON | Python |
|---|---|
object { } | dict |
array [ ] | list |
string "text" | str |
number 42, 3.5 | int, float |
true / false | True / False |
null | None |
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,nullen 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
| Terme | Sens | Fonction |
|---|---|---|
| Sérialisation (Serialization) | Données Python → texte JSON | json.dump / json.dumps |
| Désérialisation (Deserialization) | Texte JSON → données Python | json.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.
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 -> dictindent=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,uet 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? TrueLe 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
| Ligne | Explication |
|---|---|
import json | Un 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 == profile | True : 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: 1Deuxième exécution :
Added: {'id': 2, 'name': 'Dana', 'phone': '050-1234567'}
INFO Saved 2 contacts
Total contacts in file: 2Analyse :
- Le fichier contient une liste de dicts : exactement un « tableau » d’enregistrements.
load_contactsgè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 givenPour 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 serializableJSON 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: 1En 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
| Python | JSON |
|---|---|
True | true |
None | null |
"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
100Afficher 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
100dump/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
NoneAfficher 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
NoneLe 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=2pour la lisibilité,ensure_ascii=Falsepour 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.