Chapitre 07 — Les fichiers : texte, CSV et JSON¶
Durée estimée : 5 à 6 heures (cours, exercices et mini-projet)
Prérequis : Chapitre 06 — Chaînes et compréhensions (méthodes de
str,split/join, compréhensions)Objectifs — à la fin de ce chapitre, tu sauras…
- manipuler des chemins de fichiers proprement avec
pathlib;- ouvrir un fichier avec
with open(...)dans le bon mode (r,w,a) et le bon encodage (UTF-8) ;- lire un fichier texte en entier ou ligne par ligne, et écrire dedans ;
- lire et écrire des fichiers CSV avec
csv.reader,csv.DictReaderetcsv.DictWriter;- sauvegarder et recharger des données Python au format JSON ;
- rendre un programme persistant : ses données survivent à sa fermeture.
Cours¶
1. Pourquoi des fichiers ?¶
Jusqu'ici, toutes les données de tes programmes disparaissaient à la fin de l'exécution. Ta liste de courses du chapitre 04, aussi belle soit-elle, repartait de zéro à chaque lancement. C'est parce que les variables vivent dans la mémoire vive (RAM) de l'ordinateur, qui est vidée quand le programme se termine.
Analogie : la mémoire vive, c'est ton bureau : rapide d'accès, mais on le débarrasse le soir. Un fichier, c'est un classeur rangé dans une armoire : un peu plus long à consulter, mais il est encore là le lendemain.
Les fichiers servent à :
- sauvegarder des données (scores, contacts, paramètres) pour les retrouver plus tard ;
- lire des données produites ailleurs (un export de tableur, un fichier de configuration…) ;
- échanger avec d'autres programmes : le format CSV s'ouvre dans Excel ou LibreOffice, le format JSON est la langue commune du web.
Dans ce chapitre, on travaille avec trois sortes de fichiers texte :
| Format | Extension | Usage | Exemple de contenu |
|---|---|---|---|
| texte brut | .txt |
notes, journaux, listes simples | Acheter du pain |
| CSV | .csv |
tableaux (lignes et colonnes) | nom;age;ville |
| JSON | .json |
données structurées (listes, dictionnaires) | {"nom": "Alice", "age": 30} |
2. Les chemins de fichiers avec pathlib¶
Avant d'ouvrir un fichier, il faut savoir où il se trouve. C'est le rôle du chemin (path).
Chemins absolus et relatifs¶
- Un chemin absolu part de la racine du disque :
/home/alice/projets/notes.txt(Linux, macOS) ouC:\Users\alice\projets\notes.txt(Windows). - Un chemin relatif part du répertoire courant, c'est-à-dire le dossier depuis lequel tu as lancé ton programme :
notes.txt,donnees/contacts.json.
Attention : le répertoire courant est le dossier ouvert dans ton terminal au moment où tu tapes
python mon_script.py, pas forcément le dossier où se trouve le script ! Dans VS Code, c'est généralement le dossier ouvert dans l'explorateur (cf. chapitre 00). On verra plus bas comment construire un chemin fiable.
Le module pathlib et l'objet Path¶
Les chemins s'écrivent différemment selon le système (/ sous Linux et macOS, \ sous Windows). Le module pathlib de la bibliothèque standard règle ce problème : on manipule des objets Path qui s'adaptent tout seuls au système.
On l'importe avec une ligne from ... import ... en haut du fichier (les imports seront détaillés au chapitre 09) :
from pathlib import Path
dossier = Path("donnees")
fichier = dossier / "contacts.json" # l'opérateur / assemble les morceaux de chemin
print(fichier) # → donnees/contacts.json
print(fichier.name) # → contacts.json
print(fichier.stem) # → contacts
print(fichier.suffix) # → .json
print(fichier.parent) # → donnees
(Sous Windows, le premier print affichera donnees\contacts.json : c'est justement l'intérêt de pathlib.)
L'opérateur / est la grande astuce de pathlib : il colle les morceaux de chemin avec le bon séparateur. Plus besoin de concaténer des chaînes à la main.
| Attribut ou méthode | Résultat pour Path("donnees/contacts.json") |
|---|---|
.name |
contacts.json (nom complet) |
.stem |
contacts (nom sans extension) |
.suffix |
.json (extension) |
.parent |
donnees (dossier parent) |
.exists() |
True si le fichier ou dossier existe |
.is_file() / .is_dir() |
True si c'est un fichier / un dossier |
.with_suffix(".csv") |
donnees/contacts.csv (change l'extension) |
Quelques chemins utiles¶
from pathlib import Path
print(Path.cwd()) # le répertoire courant (current working directory)
print(Path.home()) # ton dossier personnel, ex. /home/alice ou C:\Users\alice
Pour obtenir le dossier où se trouve ton script (et non le répertoire courant), on utilise la variable spéciale __file__ :
from pathlib import Path
DOSSIER_SCRIPT = Path(__file__).parent
FICHIER_DONNEES = DOSSIER_SCRIPT / "contacts.json"
Ainsi, ton programme trouvera toujours contacts.json à côté du script, quel que soit le dossier d'où tu le lances. C'est la méthode recommandée pour les vrais projets. (Ce code ne fonctionne que dans un fichier .py, pas dans la console interactive.)
Créer des dossiers, tester l'existence, lister¶
from pathlib import Path
dossier = Path("essais") / "sous_dossier"
dossier.mkdir(parents=True, exist_ok=True) # crée aussi les dossiers parents, sans erreur s'il existe déjà
print(dossier.exists()) # → True
print(dossier.is_dir()) # → True
print(Path("inexistant.txt").exists()) # → False
parents=True: crée tous les dossiers intermédiaires manquants (iciessais).exist_ok=True: ne provoque pas d'erreur si le dossier existe déjà.
Pour lister le contenu d'un dossier, iterdir() renvoie tous les éléments et glob("*.txt") ceux qui correspondent à un motif (* signifie « n'importe quoi ») :
from pathlib import Path
dossier = Path("essais")
for nom in ["a.txt", "b.txt", "c.csv"]:
(dossier / nom).write_text("contenu\n", encoding="utf-8") # crée des fichiers (expliqué plus bas)
print(sorted(p.name for p in dossier.iterdir()))
print(sorted(p.name for p in dossier.glob("*.txt")))
L'ordre renvoyé par iterdir et glob n'est pas garanti : on utilise sorted pour un résultat stable. glob("**/*.txt") cherche aussi dans les sous-dossiers.
3. Ouvrir un fichier : open, with et les modes¶
La fonction open¶
Pour lire ou écrire dans un fichier, on l'ouvre avec open(chemin, mode, encoding=...). On obtient un objet « fichier » sur lequel on peut lire ou écrire. Puis il faut le fermer, pour que tout soit bien enregistré et que le fichier soit libéré.
Le bloc with : la bonne façon de faire¶
On ouvre toujours un fichier avec with :
from pathlib import Path
chemin = Path("bonjour.txt")
with open(chemin, "w", encoding="utf-8") as fichier:
fichier.write("Bonjour le monde !\n")
fichier.write("Deuxième ligne, avec des accents : é è ç ù.\n")
print("Fichier écrit, et déjà fermé.")
Lis-le ainsi : « avec le fichier ouvert sous le nom fichier, fais… ». Dès qu'on sort du bloc indenté, Python ferme automatiquement le fichier, même si une erreur survient au milieu. C'est comme une porte qui se referme toute seule derrière toi.
Ouvre maintenant bonjour.txt dans VS Code : tu y verras tes deux lignes.
Les modes d'ouverture¶
Le deuxième argument de open indique ce qu'on veut faire :
| Mode | Nom | Si le fichier existe | S'il n'existe pas |
|---|---|---|---|
"r" |
lecture (read), par défaut | on le lit | erreur FileNotFoundError |
"w" |
écriture (write) | son contenu est effacé ! | il est créé |
"a" |
ajout (append) | on écrit à la fin | il est créé |
"x" |
création exclusive | erreur FileExistsError |
il est créé |
Le mode "w" est dangereux : il vide le fichier dès l'ouverture, avant même d'écrire quoi que ce soit. Pour ajouter des lignes sans rien perdre, utilise "a".
L'encodage : toujours encoding="utf-8"¶
Un fichier ne contient que des octets (des nombres). L'encodage est la règle qui traduit ces octets en caractères. Le standard universel aujourd'hui est UTF-8, qui gère tous les caractères : accents, emojis, alphabets du monde entier.
Si tu ne précises rien, Python utilise l'encodage par défaut de ton système, qui n'est pas forcément UTF-8 (sous Windows, c'est souvent cp1252). Résultat : un fichier écrit sur un ordinateur peut afficher é au lieu de é sur un autre. Voici ce qui se passe quand on relit un fichier UTF-8 avec le mauvais encodage :
from pathlib import Path
with open(Path("bonjour.txt"), "r", encoding="latin-1") as fichier:
print(fichier.read())
Règle d'or de ce chapitre : écris toujours encoding="utf-8" quand tu ouvres un fichier texte. Pour lire comme pour écrire.
4. Lire un fichier texte¶
Préparons un fichier de travail :
from pathlib import Path
with open(Path("fruits.txt"), "w", encoding="utf-8") as fichier:
fichier.write("pomme\n")
fichier.write("banane\n")
fichier.write("cerise\n")
Il existe plusieurs façons de lire, selon le besoin.
Tout lire d'un coup : read()¶
from pathlib import Path
with open(Path("fruits.txt"), "r", encoding="utf-8") as fichier:
contenu = fichier.read()
print(repr(contenu))
print(contenu)
read() renvoie une seule chaîne contenant tout le fichier, retours à la ligne \n compris. repr() permet de voir ces caractères invisibles (d'où la ligne vide à la fin du second affichage : c'est le dernier \n, plus celui ajouté par print).
Comme on lit, on pourrait omettre "r" (c'est le mode par défaut), mais l'écrire rend le code plus explicite.
Ligne par ligne : boucler sur le fichier¶
Un objet fichier se parcourt avec for, ligne par ligne. C'est la méthode à privilégier, surtout pour les gros fichiers, car Python ne charge qu'une ligne à la fois en mémoire.
from pathlib import Path
with open(Path("fruits.txt"), "r", encoding="utf-8") as fichier:
for numero, ligne in enumerate(fichier, start=1):
print(f"{numero}: {ligne}")
Pourquoi ces lignes vides ? Chaque ligne se termine par \n, et print en ajoute un autre. On retire donc presque toujours le retour à la ligne avec strip() (ou rstrip("\n") pour ne retirer que lui) :
from pathlib import Path
with open(Path("fruits.txt"), "r", encoding="utf-8") as fichier:
for numero, ligne in enumerate(fichier, start=1):
ligne = ligne.strip()
print(f"{numero}: {ligne}")
Obtenir une liste de lignes¶
Pour récupérer une liste de lignes propres, la façon la plus simple combine read() et splitlines() (vue au chapitre 06), qui découpe sur les retours à la ligne et les supprime :
from pathlib import Path
with open(Path("fruits.txt"), "r", encoding="utf-8") as fichier:
fruits = fichier.read().splitlines()
print(fruits) # → ['pomme', 'banane', 'cerise']
Il existe aussi fichier.readlines(), qui renvoie une liste de lignes avec leur \n final, et fichier.readline() (sans s), qui lit une seule ligne à la fois.
Les raccourcis de pathlib : read_text et write_text¶
Pour les cas simples (lire ou écrire tout un fichier d'un coup), un objet Path propose deux méthodes qui ouvrent et ferment le fichier pour toi :
from pathlib import Path
chemin = Path("message.txt")
chemin.write_text("Salut !\nÀ bientôt.\n", encoding="utf-8") # mode "w" : écrase !
texte = chemin.read_text(encoding="utf-8")
print(texte.splitlines()) # → ['Salut !', 'À bientôt.']
C'est très pratique, mais write_text remplace tout le contenu, comme le mode "w". Pour ajouter à la fin ou traiter un gros fichier ligne par ligne, reviens à with open(...).
Que se passe-t-il si le fichier n'existe pas ?¶
from pathlib import Path
with open(Path("inexistant.txt"), "r", encoding="utf-8") as fichier:
print(fichier.read())
Traceback (most recent call last):
File "<stdin>", line 3, in <module>
FileNotFoundError: [Errno 2] No such file or directory: 'inexistant.txt'
Pour l'instant, la solution est de vérifier avant avec exists() :
from pathlib import Path
chemin = Path("inexistant.txt")
if chemin.exists():
print(chemin.read_text(encoding="utf-8"))
else:
print(f"Le fichier {chemin} n'existe pas encore.") # → Le fichier inexistant.txt n'existe pas encore.
Au chapitre 08, tu apprendras à intercepter ce genre d'erreur avec
try/except, ce qui est souvent plus robuste.
5. Écrire dans un fichier¶
write() : écrire une chaîne¶
fichier.write(chaine) écrit la chaîne telle quelle : il n'ajoute pas de retour à la ligne, contrairement à print. Il faut mettre les \n soi-même.
from pathlib import Path
with open(Path("sans_retour.txt"), "w", encoding="utf-8") as fichier:
fichier.write("un")
fichier.write("deux")
fichier.write("trois")
print(Path("sans_retour.txt").read_text(encoding="utf-8")) # → undeuxtrois
write() n'accepte que des chaînes : pour écrire un nombre, convertis-le avec str() ou une f-string.
from pathlib import Path
notes = [12, 15.5, 9]
with open(Path("notes.txt"), "w", encoding="utf-8") as fichier:
for note in notes:
fichier.write(f"{note}\n")
print(Path("notes.txt").read_text(encoding="utf-8").splitlines()) # → ['12', '15.5', '9']
Pour écrire une liste de lignes d'un coup, utilise join :
from pathlib import Path
villes = ["Paris", "Lyon", "Marseille"]
with open(Path("villes.txt"), "w", encoding="utf-8") as fichier:
fichier.write("\n".join(villes) + "\n")
print(Path("villes.txt").read_text(encoding="utf-8").splitlines()) # → ['Paris', 'Lyon', 'Marseille']
print(..., file=...) : écrire comme on affiche¶
Tu peux aussi utiliser print en lui indiquant un fichier : il ajoute alors automatiquement les espaces et le retour à la ligne, comme à l'écran.
from pathlib import Path
with open(Path("rapport.txt"), "w", encoding="utf-8") as fichier:
print("Rapport du jour", file=fichier)
print("Total :", 42, "ventes", file=fichier)
print(Path("rapport.txt").read_text(encoding="utf-8"), end="")
Le mode "a" : ajouter à la fin¶
Le mode ajout est idéal pour un journal (log) : chaque exécution ajoute des lignes sans effacer les précédentes.
from pathlib import Path
journal = Path("journal.txt")
with open(journal, "w", encoding="utf-8") as fichier: # on repart d'un fichier vide
fichier.write("Démarrage\n")
with open(journal, "a", encoding="utf-8") as fichier: # ajout
fichier.write("Connexion d'Alice\n")
with open(journal, "a", encoding="utf-8") as fichier: # nouvel ajout
fichier.write("Connexion de Bob\n")
print(journal.read_text(encoding="utf-8"), end="")
Si on avait utilisé "w" à chaque fois, il ne resterait que la dernière ligne.
Exemple complet : traiter un fichier et en écrire un autre¶
Un schéma très courant : lire un fichier, transformer chaque ligne, écrire le résultat dans un autre fichier. Ici, on nettoie une liste d'inscrits (espaces superflus, lignes vides, majuscules incohérentes, doublons).
from pathlib import Path
brut = Path("inscrits_brut.txt")
brut.write_text(" alice MARTIN\n\nBob durand \nalice martin\n \nchloé petit\n", encoding="utf-8")
propres = []
with open(brut, "r", encoding="utf-8") as entree:
for ligne in entree:
nom = " ".join(ligne.split()).title() # espaces normalisés + majuscules
if nom != "" and nom not in propres: # ignore lignes vides et doublons
propres.append(nom)
with open(Path("inscrits.txt"), "w", encoding="utf-8") as sortie:
for nom in sorted(propres):
sortie.write(nom + "\n")
print(Path("inscrits.txt").read_text(encoding="utf-8"), end="")
6. Les fichiers CSV¶
Le format CSV¶
CSV signifie Comma-Separated Values, « valeurs séparées par des virgules ». C'est le format le plus simple pour stocker un tableau : une ligne du fichier par ligne du tableau, et un séparateur entre les colonnes. La première ligne contient généralement les en-têtes (noms des colonnes).
En France, les tableurs utilisent souvent le point-virgule ; comme séparateur, car la virgule sert de séparateur décimal (12,5). Tous les tableurs (Excel, LibreOffice Calc, Google Sheets) savent ouvrir et exporter du CSV.
Pourquoi ne pas simplement utiliser split(",") ? Parce qu'une valeur peut elle-même contenir une virgule ou un retour à la ligne. Le format CSV prévoit alors de l'entourer de guillemets : "Dupont, Jean",42. Un split naïf couperait au mauvais endroit. Le module csv de la bibliothèque standard gère tous ces cas pour toi.
Écrire un CSV avec csv.writer¶
import csv
from pathlib import Path
eleves = [
["nom", "note", "classe"],
["Alice", 15.5, "3A"],
["Dupont, Jean", 12, "3B"],
["Chloé", 18, "3A"],
]
with open(Path("eleves.csv"), "w", encoding="utf-8", newline="") as fichier:
ecrivain = csv.writer(fichier)
ecrivain.writerows(eleves) # écrit toutes les lignes d'un coup
print(Path("eleves.csv").read_text(encoding="utf-8"), end="")
Points importants :
newline=""est obligatoire pour les fichiers CSV : le modulecsvgère lui-même les fins de ligne. Sans cela, sous Windows, tu obtiendrais une ligne vide entre chaque ligne.writerow(liste)écrit une ligne,writerows(liste_de_listes)en écrit plusieurs.- Les nombres sont convertis automatiquement en texte.
- La valeur contenant une virgule a été entourée de guillemets automatiquement.
Lire un CSV avec csv.reader¶
import csv
from pathlib import Path
with open(Path("eleves.csv"), "r", encoding="utf-8", newline="") as fichier:
lecteur = csv.reader(fichier)
for ligne in lecteur:
print(ligne)
['nom', 'note', 'classe']
['Alice', '15.5', '3A']
['Dupont, Jean', '12', '3B']
['Chloé', '18', '3A']
Chaque ligne devient une liste de chaînes. Remarque deux choses :
- Les guillemets autour de
Dupont, Jeanont été correctement interprétés. - Tout est du texte :
'15.5'et'12'sont des chaînes ! Pour calculer, il faut convertir avecfloat()ouint().
Pour sauter la ligne d'en-têtes, on peut utiliser next(lecteur), qui lit la ligne suivante :
import csv
from pathlib import Path
with open(Path("eleves.csv"), "r", encoding="utf-8", newline="") as fichier:
lecteur = csv.reader(fichier)
en_tetes = next(lecteur) # consomme la première ligne
notes = [float(ligne[1]) for ligne in lecteur]
print(en_tetes) # → ['nom', 'note', 'classe']
print(notes) # → [15.5, 12.0, 18.0]
print(f"Moyenne : {sum(notes) / len(notes):.2f}") # → Moyenne : 15.17
Changer de séparateur : delimiter¶
Pour un CSV « à la française » avec des points-virgules, on précise delimiter=";", à la lecture comme à l'écriture :
import csv
from pathlib import Path
Path("depenses.csv").write_text(
"date;libelle;montant\n2024-03-01;Courses;54,20\n2024-03-03;Cinéma;11,50\n",
encoding="utf-8",
)
total = 0
with open(Path("depenses.csv"), "r", encoding="utf-8", newline="") as fichier:
lecteur = csv.reader(fichier, delimiter=";")
next(lecteur)
for date, libelle, montant in lecteur:
valeur = float(montant.replace(",", ".")) # 54,20 → 54.20
total += valeur
print(f"{date} {libelle:<10} {valeur:>7.2f}")
print(f"Total : {total:.2f} €")
Note le déballage for date, libelle, montant in lecteur et la conversion de la virgule décimale en point, indispensable avant float().
Lire avec csv.DictReader : accéder aux colonnes par leur nom¶
Accéder aux colonnes par indice (ligne[1]) n'est pas très lisible, et casse dès qu'on ajoute une colonne. csv.DictReader utilise la ligne d'en-têtes pour transformer chaque ligne en dictionnaire :
import csv
from pathlib import Path
with open(Path("eleves.csv"), "r", encoding="utf-8", newline="") as fichier:
lecteur = csv.DictReader(fichier)
for eleve in lecteur:
print(eleve)
{'nom': 'Alice', 'note': '15.5', 'classe': '3A'}
{'nom': 'Dupont, Jean', 'note': '12', 'classe': '3B'}
{'nom': 'Chloé', 'note': '18', 'classe': '3A'}
C'est beaucoup plus parlant : eleve["note"] au lieu de eleve[1]. La ligne d'en-têtes est consommée automatiquement. Et on retrouve la liste de dictionnaires du chapitre 04 :
import csv
from pathlib import Path
with open(Path("eleves.csv"), "r", encoding="utf-8", newline="") as fichier:
eleves = list(csv.DictReader(fichier))
classe_3a = [e["nom"] for e in eleves if e["classe"] == "3A"]
meilleur = max(eleves, key=lambda e: float(e["note"]))
print(classe_3a) # → ['Alice', 'Chloé']
print(meilleur["nom"]) # → Chloé
Écrire avec csv.DictWriter¶
Symétriquement, csv.DictWriter écrit une liste de dictionnaires. On lui indique la liste des colonnes avec fieldnames, et on écrit la ligne d'en-têtes avec writeheader() :
import csv
from pathlib import Path
produits = [
{"nom": "Stylo", "prix": 1.5, "stock": 120},
{"nom": "Cahier", "prix": 3.2, "stock": 45},
{"nom": "Gomme", "prix": 0.8, "stock": 0},
]
colonnes = ["nom", "prix", "stock"]
with open(Path("produits.csv"), "w", encoding="utf-8", newline="") as fichier:
ecrivain = csv.DictWriter(fichier, fieldnames=colonnes, delimiter=";")
ecrivain.writeheader()
ecrivain.writerows(produits)
print(Path("produits.csv").read_text(encoding="utf-8"), end="")
- L'ordre des colonnes dans le fichier est celui de
fieldnames. - Si un dictionnaire contient une clé absente de
fieldnames,DictWriterprovoque une erreurValueError(on peut ajouterextrasaction="ignore"pour l'ignorer). - Si une clé manque dans un dictionnaire, la case reste vide.
Ajouter des lignes à un CSV existant¶
On combine le mode "a" et DictWriter, sans réécrire les en-têtes :
import csv
from pathlib import Path
nouveau = {"nom": "Règle", "prix": 2.1, "stock": 30}
with open(Path("produits.csv"), "a", encoding="utf-8", newline="") as fichier:
ecrivain = csv.DictWriter(fichier, fieldnames=["nom", "prix", "stock"], delimiter=";")
ecrivain.writerow(nouveau)
print(Path("produits.csv").read_text(encoding="utf-8"), end="")
Et Excel ? Excel a parfois du mal à détecter l'UTF-8 et affiche alors des caractères bizarres. Si un fichier doit être ouvert par Excel, utilise
encoding="utf-8-sig"à l'écriture : cela ajoute une petite signature invisible au début du fichier que Excel reconnaît. Python relit sans problème ce fichier avecencoding="utf-8-sig".
7. Les fichiers JSON¶
Le format JSON¶
JSON (JavaScript Object Notation) est un format texte pour représenter des données structurées : des listes et des dictionnaires imbriqués. C'est la langue commune des applications web et de très nombreux fichiers de configuration. Bonne nouvelle : il ressemble énormément à la syntaxe Python !
{
"nom": "Alice",
"age": 30,
"abonne": true,
"telephone": null,
"loisirs": ["lecture", "escalade"],
"adresse": {"ville": "Lyon", "code_postal": "69001"}
}
Correspondance entre JSON et Python :
| JSON | Python |
|---|---|
objet {...} |
dict |
tableau [...] |
list |
chaîne "..." (guillemets doubles obligatoires) |
str |
nombre 42, 3.14 |
int, float |
true / false |
True / False |
null |
None |
Contrairement au CSV, qui ne stocke que des tableaux de textes, le JSON conserve la structure et les types (nombres, booléens, listes imbriquées…). C'est le format idéal pour sauvegarder les données d'un programme.
Sauvegarder avec json.dump¶
import json
from pathlib import Path
contact = {
"nom": "Chloé",
"age": 28,
"abonne": True,
"telephone": None,
"loisirs": ["piano", "randonnée"],
}
with open(Path("contact.json"), "w", encoding="utf-8") as fichier:
json.dump(contact, fichier, ensure_ascii=False, indent=4)
print(Path("contact.json").read_text(encoding="utf-8"))
{
"nom": "Chloé",
"age": 28,
"abonne": true,
"telephone": null,
"loisirs": [
"piano",
"randonnée"
]
}
json.dump(donnees, fichier, ...) convertit les données Python en texte JSON et les écrit dans le fichier. Deux options à utiliser systématiquement :
indent=4: met en forme le JSON sur plusieurs lignes avec une indentation, pour qu'il soit lisible par un humain. Sans cela, tout est écrit sur une seule ligne.ensure_ascii=False: écrit les caractères accentués tels quels. Sans cette option, Python remplace chaque caractère non-ASCII par un code, comme\u00e9pouré: le fichier reste valide, mais devient illisible pour un humain.
Compare :
import json
print(json.dumps({"ville": "Besançon"}))
print(json.dumps({"ville": "Besançon"}, ensure_ascii=False))
(json.dumps, avec un s pour string, renvoie le JSON sous forme de chaîne au lieu de l'écrire dans un fichier.)
Charger avec json.load¶
import json
from pathlib import Path
with open(Path("contact.json"), "r", encoding="utf-8") as fichier:
contact = json.load(fichier)
print(type(contact)) # → <class 'dict'>
print(contact["nom"]) # → Chloé
print(contact["loisirs"]) # → ['piano', 'randonnée']
print(contact["abonne"]) # → True
print(contact["telephone"]) # → None
json.load(fichier) lit le fichier et reconstruit les objets Python : on retrouve un vrai dictionnaire, avec de vrais booléens, de vraies listes, de vrais nombres. Aucune conversion manuelle n'est nécessaire, contrairement au CSV.
Il existe aussi json.loads(chaine) (avec un s), qui analyse une chaîne JSON :
import json
donnees = json.loads('{"scores": [12, 18, 15], "joueur": "Léo"}')
print(donnees["joueur"], max(donnees["scores"])) # → Léo 18
Pour retenir : dump = de Python vers le JSON (on « vide » ses données dans le fichier), load = du JSON vers Python (on « charge »). Avec un s final, on travaille sur une chaîne au lieu d'un fichier.
Sauvegarder toute une structure¶
Le JSON gère les structures imbriquées : une liste de dictionnaires, un dictionnaire de listes… Voici le schéma typique d'un programme qui conserve ses données entre deux exécutions :
import json
from pathlib import Path
FICHIER_SCORES = Path("scores.json")
def charger_scores(chemin):
"""Renvoie le dictionnaire des scores, ou un dictionnaire vide si le fichier n'existe pas."""
if not chemin.exists():
return {}
with open(chemin, "r", encoding="utf-8") as fichier:
return json.load(fichier)
def sauvegarder_scores(scores, chemin):
"""Écrit le dictionnaire des scores dans le fichier JSON."""
with open(chemin, "w", encoding="utf-8") as fichier:
json.dump(scores, fichier, ensure_ascii=False, indent=2)
scores = charger_scores(FICHIER_SCORES)
print(f"Scores chargés : {scores}")
scores["Inès"] = scores.get("Inès", 0) + 150
scores["Noé"] = scores.get("Noé", 0) + 90
sauvegarder_scores(scores, FICHIER_SCORES)
print(f"Scores sauvegardés : {charger_scores(FICHIER_SCORES)}")
Première exécution :
Si tu relances le programme, les scores sont retrouvés et augmentés :
Ton programme a désormais une mémoire ! Retiens bien ce couple de fonctions charger / sauvegarder : tu le réutiliseras dans presque tous tes projets.
Ce que JSON ne sait pas stocker¶
JSON ne connaît que les types du tableau ci-dessus. Quelques conséquences :
- Les tuples deviennent des listes :
(1, 2)est relu comme[1, 2]. - Les ensembles (
set) ne sont pas acceptés : convertis-les en liste avant (list(mon_ensemble)). - Les clés de dictionnaire deviennent toujours des chaînes (voir les pièges).
- Les dates et les objets que tu créeras plus tard (chapitres 09 et 10) doivent être convertis à la main (en chaîne ou en dictionnaire).
import json
donnees = {"point": (3, 4), "tags": sorted({"b", "a"})}
texte = json.dumps(donnees)
print(texte) # → {"point": [3, 4], "tags": ["a", "b"]}
print(json.loads(texte)) # → {'point': [3, 4], 'tags': ['a', 'b']}
Essayer d'enregistrer un ensemble directement provoque une erreur :
Traceback (most recent call last):
File "<stdin>", line 3, in <module>
TypeError: Object of type set is not JSON serializable
8. CSV ou JSON : lequel choisir ?¶
| Critère | CSV | JSON |
|---|---|---|
| Structure | un tableau plat (lignes × colonnes) | arbitraire : listes et dictionnaires imbriqués |
| Types | tout est texte, il faut convertir | nombres, booléens, null conservés |
| Lisible par un tableur | oui (Excel, LibreOffice) | non |
| Usage typique | exports, données tabulaires, échanges avec des non-programmeurs | sauvegarde de l'état d'un programme, configuration, web (API) |
En pratique : si tes données ressemblent à un tableau et doivent être ouvertes dans un tableur, choisis le CSV. Si elles sont structurées (un contact avec plusieurs téléphones, des paramètres imbriqués), ou s'il s'agit simplement de sauvegarder les variables de ton programme, choisis le JSON.
On peut d'ailleurs passer facilement de l'un à l'autre : csv.DictReader produit une liste de dictionnaires, que json.dump sait écrire, et inversement (c'est l'exercice 7).
Pièges fréquents¶
Piège 1 — Oublier encoding="utf-8"¶
from pathlib import Path
Path("accents.txt").write_text("Crème brûlée\n", encoding="utf-8")
with open(Path("accents.txt"), "r", encoding="ascii") as fichier: # mauvais encodage
print(fichier.read())
Traceback (most recent call last):
File "<stdin>", line 5, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 2: ordinal not in range(128)
Selon le système et l'encodage utilisé, on obtient soit une UnicodeDecodeError comme ici, soit des caractères bizarres (Crème brûlée). Si tu n'indiques pas d'encodage, Python utilise celui de ton système, et ton programme peut fonctionner chez toi mais pas chez quelqu'un d'autre. Correction : toujours encoding="utf-8", en lecture et en écriture.
Piège 2 — Écraser un fichier avec le mode "w"¶
from pathlib import Path
with open(Path("journal.txt"), "w", encoding="utf-8") as fichier:
fichier.write("Nouvelle entrée\n")
print(Path("journal.txt").read_text(encoding="utf-8"), end="") # → Nouvelle entrée
Toutes les entrées précédentes du journal ont disparu, sans aucun avertissement. Correction : utiliser le mode "a" pour ajouter. En cas de doute, vérifie avec exists() avant d'écrire, ou utilise "x" qui refuse d'écraser un fichier existant.
Piège 3 — Oublier les \n avec write¶
from pathlib import Path
with open(Path("courses.txt"), "w", encoding="utf-8") as fichier:
for article in ["pain", "lait", "oeufs"]:
fichier.write(article)
print(Path("courses.txt").read_text(encoding="utf-8")) # → painlaitoeufs
Correction : fichier.write(article + "\n"), ou print(article, file=fichier).
Piège 4 — Oublier de retirer les \n à la lecture¶
from pathlib import Path
Path("mdp.txt").write_text("secret123\n", encoding="utf-8")
with open(Path("mdp.txt"), "r", encoding="utf-8") as fichier:
mot_de_passe = fichier.readline()
print(mot_de_passe == "secret123") # → False
print(repr(mot_de_passe)) # → 'secret123\n'
Le \n fait partie de la ligne lue ! Correction : fichier.readline().strip(). Quand une comparaison échoue mystérieusement, affiche la valeur avec repr() pour voir les caractères invisibles.
Piège 5 — Calculer avec des valeurs CSV sans les convertir¶
import csv
from pathlib import Path
Path("ventes.csv").write_text("produit,quantite\nA,3\nB,5\n", encoding="utf-8")
with open(Path("ventes.csv"), "r", encoding="utf-8", newline="") as fichier:
total = 0
for ligne in csv.DictReader(fichier):
total += ligne["quantite"]
Traceback (most recent call last):
File "<stdin>", line 7, in <module>
TypeError: unsupported operand type(s) for +=: 'int' and 'str'
Le module csv renvoie toujours des chaînes. Correction : total += int(ligne["quantite"]).
Piège 6 — Oublier newline="" avec le module csv¶
Sous Linux et macOS, on ne voit pas la différence. Mais sous Windows, sans newline="", chaque ligne écrite par csv.writer est suivie d'une ligne vide quand on ouvre le fichier dans un tableur. Correction : ouvre toujours un fichier CSV avec open(chemin, mode, encoding="utf-8", newline=""), en lecture comme en écriture.
Piège 7 — Lire deux fois le même fichier ouvert¶
from pathlib import Path
with open(Path("fruits.txt"), "r", encoding="utf-8") as fichier:
premiere_lecture = fichier.read()
deuxieme_lecture = fichier.read()
print(len(premiere_lecture)) # → 20
print(repr(deuxieme_lecture)) # → ''
Un fichier ouvert a un curseur, comme une tête de lecture : après le premier read(), il est à la fin et il n'y a plus rien à lire. Même chose si tu boucles deux fois sur un fichier ou un csv.reader. Correction : stocke le contenu dans une variable (une liste, par exemple) et réutilise-la, ou rouvre le fichier.
Piège 8 — Les clés JSON deviennent des chaînes¶
import json
niveaux = {1: "facile", 2: "moyen"}
relu = json.loads(json.dumps(niveaux))
print(relu) # → {'1': 'facile', '2': 'moyen'}
print(relu.get(1)) # → None
En JSON, les clés d'un objet sont toujours des chaînes. Après un aller-retour, 1 est devenu "1", et relu[1] provoquerait une KeyError. Correction : utiliser des chaînes comme clés dès le départ, ou reconvertir après chargement : {int(cle): valeur for cle, valeur in relu.items()}.
Exercices¶
Pour chaque exercice, le corrigé commence par créer les fichiers de données nécessaires : tu peux donc les tester dans un dossier vide.
Exercice 1 — Écrire puis relire ⭐¶
- Écris dans
table7.txtla table de multiplication de 7, de 1 à 10, une opération par ligne (7 x 1 = 7). - Relis le fichier et affiche seulement les lignes dont le résultat est pair, précédées de leur numéro de ligne.
Sortie attendue :
Ligne 2 : 7 x 2 = 14
Ligne 4 : 7 x 4 = 28
Ligne 6 : 7 x 6 = 42
Ligne 8 : 7 x 8 = 56
Ligne 10 : 7 x 10 = 70
Exercice 2 — Compteur de fichier ⭐¶
Écris une fonction statistiques_fichier(chemin) qui renvoie un dictionnaire avec le nombre de lignes, de mots et de caractères d'un fichier texte (comme la commande wc de Linux), en comptant les caractères sans les retours à la ligne. Teste-la sur un fichier poeme.txt contenant :
Demain, dès l'aube, à l'heure où blanchit la campagne,
Je partirai. Vois-tu, je sais que tu m'attends.
J'irai par la forêt, j'irai par la montagne.
Sortie attendue :
Exercice 3 — Nettoyer un fichier de configuration ⭐⭐¶
Un fichier config.txt contient des réglages cle = valeur, des lignes vides et des commentaires commençant par # :
Écris une fonction lire_config(chemin) qui renvoie un dictionnaire des réglages, en ignorant les commentaires et les lignes vides, et en nettoyant les espaces autour des clés et des valeurs. Indice : ligne.split("=", 1) coupe uniquement au premier =.
Exercice 4 — Bulletin à partir d'un CSV ⭐⭐¶
Le fichier notes.csv (séparateur ;) contient une ligne par élève :
Avec csv.DictReader et csv.DictWriter, crée un fichier bulletin.csv (séparateur ;) avec les colonnes nom, moyenne (arrondie à 2 décimales) et resultat (admis si moyenne ≥ 10, sinon ajourné), trié par moyenne décroissante. Affiche ensuite son contenu.
Sortie attendue :
Exercice 5 — Préférences en JSON ⭐⭐¶
Écris un programme qui gère un fichier preferences.json :
- une constante PREFERENCES_PAR_DEFAUT = {"theme": "clair", "langue": "fr", "taille_police": 12} ;
- une fonction charger_preferences(chemin) qui renvoie les préférences du fichier s'il existe, complétées par les valeurs par défaut pour les clés manquantes ; si le fichier n'existe pas, elle renvoie une copie des valeurs par défaut ;
- une fonction sauvegarder_preferences(preferences, chemin) (JSON lisible, accents conservés).
Teste : supprime le fichier s'il existe (chemin.unlink(missing_ok=True)), charge, passe le thème à "sombre", sauvegarde ; écris ensuite à la main un fichier ne contenant que {"langue": "en"} et recharge.
Au départ : {'theme': 'clair', 'langue': 'fr', 'taille_police': 12}
Après sauvegarde : {'theme': 'sombre', 'langue': 'fr', 'taille_police': 12}
Fichier partiel : {'theme': 'clair', 'langue': 'en', 'taille_police': 12}
Exercice 6 — Inventaire d'un dossier ⭐⭐¶
Crée un dossier inventaire contenant les fichiers rapport.txt, photo.jpg, budget.csv, notes.txt et vacances.jpg (contenu au choix, par exemple leur propre nom). Écris une fonction inventaire_par_extension(dossier) qui renvoie un dictionnaire {extension: [noms de fichiers triés]}, puis affiche-le par extension triée.
Sortie attendue :
Exercice 7 — Convertisseur CSV ↔ JSON ⭐⭐⭐¶
Écris deux fonctions :
- csv_vers_json(chemin_csv, chemin_json) : lit un CSV (séparateur ,) avec DictReader et écrit la liste de dictionnaires en JSON. Les valeurs qui ne contiennent que des chiffres doivent être converties en int ;
- json_vers_csv(chemin_json, chemin_csv) : fait l'inverse avec DictWriter, en prenant comme colonnes les clés du premier élément.
Teste avec un fichier villes.csv :
Puis reconvertis le JSON en villes_copie.csv et vérifie que le contenu est identique à l'original.
Sortie attendue :
[
{
"ville": "Lyon",
"departement": "Rhône",
"habitants": 522250
},
{
"ville": "Lille",
"departement": "Nord",
"habitants": 236710
},
{
"ville": "Nantes",
"departement": "Loire-Atlantique",
"habitants": 320732
}
]
Total habitants : 1079692
Copie identique : True
Exercice 8 — Rapport de ventes ⭐⭐⭐¶
Le fichier ventes.csv liste des ventes :
date,produit,categorie,quantite,prix_unitaire
2024-05-02,Clavier,Informatique,2,49.90
2024-05-02,Stylo,Papeterie,10,1.20
2024-05-03,Souris,Informatique,3,19.90
2024-05-03,Cahier,Papeterie,5,3.50
2024-05-04,Clavier,Informatique,1,49.90
2024-05-04,Casque,Audio,1,89.00
Écris un programme qui :
1. calcule le chiffre d'affaires (quantité × prix) par catégorie et le nombre d'articles vendus par catégorie ;
2. écrit rapport_ventes.csv avec les colonnes categorie, articles, chiffre_affaires (2 décimales), trié par chiffre d'affaires décroissant ;
3. écrit rapport_ventes.json contenant {"total": ..., "meilleur_produit": ..., "categories": {...}}, où meilleur_produit est le produit au plus gros chiffre d'affaires cumulé ;
4. affiche le contenu des deux fichiers.
Sortie attendue :
categorie,articles,chiffre_affaires
Informatique,6,209.40
Audio,1,89.00
Papeterie,15,29.50
{
"total": 327.9,
"meilleur_produit": "Clavier",
"categories": {
"Informatique": 209.4,
"Papeterie": 29.5,
"Audio": 89.0
}
}
Mini-projet — Le carnet de contacts sauvegardé en JSON¶
Cahier des charges¶
Écris un carnet de contacts en ligne de commande, dont les données survivent à la fermeture du programme grâce à un fichier contacts.json.
Chaque contact est un dictionnaire :
Le carnet entier est une liste de ces dictionnaires. Le programme affiche un menu en boucle :
=== CARNET DE CONTACTS (2 contacts) ===
1. Lister les contacts
2. Ajouter un contact
3. Rechercher
4. Supprimer un contact
5. Exporter en CSV
6. Quitter
- Au démarrage, les contacts sont chargés depuis
contacts.json(liste vide si le fichier n'existe pas). - Lister : affiche les contacts triés par nom puis prénom, numérotés, dans un tableau aligné.
- Ajouter : demande nom, prénom, téléphone et email ; nom et prénom sont obligatoires (on redemande s'ils sont vides). Le nom est mis en majuscules, le prénom avec une majuscule initiale. Sauvegarde immédiate.
- Rechercher : demande un texte et affiche les contacts dont le nom, le prénom ou l'email le contient (sans tenir compte de la casse).
- Supprimer : affiche la liste numérotée, demande un numéro, demande confirmation (
o/n), supprime et sauvegarde. - Exporter : écrit tous les contacts dans
contacts.csv(séparateur;, encodageutf-8-sigpour Excel) avecDictWriter. - Quitter : message d'au revoir.
Étapes suggérées¶
- Écris
charger_contacts(chemin)etsauvegarder_contacts(contacts, chemin)(sur le modèle des scores de la section 7) et teste-les seules. - Écris
afficher_contacts(contacts)avec un tableau aligné par f-strings etsorted(..., key=lambda c: (c["nom"], c["prenom"])). - Écris
demander_non_vide(message)puisajouter_contact(contacts). - Écris
rechercher(contacts, texte)qui renvoie la liste des résultats (sansprint). - Écris
supprimer_contact(contacts)en vérifiant que le numéro saisi est valide (isdigit()et dans les bornes). - Écris
exporter_csv(contacts, chemin). - Assemble le tout dans
main()avec le menu.
Pistes d'amélioration¶
- Modifier un contact existant.
- Refuser les doublons (même nom et même prénom).
- Vérifier le format de l'email (présence d'un
@et d'un.après) et du téléphone (10 chiffres). - Permettre plusieurs numéros par contact (une liste dans le dictionnaire : impossible en CSV simple, facile en JSON !).
- Importer des contacts depuis un CSV.
- Faire une sauvegarde de sécurité
contacts.bak.jsonavant chaque écriture. - Gérer un fichier JSON corrompu avec
try/except(chapitre 08), puis transformer les contacts en objets (chapitre 10).
Corrigés¶
Corrigé de l'exercice 1
from pathlib import Path
chemin = Path("table7.txt")
# 1. Écriture
with open(chemin, "w", encoding="utf-8") as fichier:
for i in range(1, 11):
fichier.write(f"7 x {i} = {7 * i}\n")
# 2. Relecture et filtrage
with open(chemin, "r", encoding="utf-8") as fichier:
for numero, ligne in enumerate(fichier, start=1):
ligne = ligne.strip()
resultat = int(ligne.split("=")[1])
if resultat % 2 == 0:
print(f"Ligne {numero} : {ligne}")
Corrigé de l'exercice 2
from pathlib import Path
def statistiques_fichier(chemin):
"""Renvoie le nombre de lignes, de mots et de caractères (hors retours à la ligne)."""
nb_lignes = 0
nb_mots = 0
nb_caracteres = 0
with open(chemin, "r", encoding="utf-8") as fichier:
for ligne in fichier:
ligne = ligne.rstrip("\n")
nb_lignes += 1
nb_mots += len(ligne.split())
nb_caracteres += len(ligne)
return {"lignes": nb_lignes, "mots": nb_mots, "caracteres": nb_caracteres}
poeme = Path("poeme.txt")
poeme.write_text(
"Demain, dès l'aube, à l'heure où blanchit la campagne,\n"
"Je partirai. Vois-tu, je sais que tu m'attends.\n"
"J'irai par la forêt, j'irai par la montagne.\n",
encoding="utf-8",
)
print(statistiques_fichier(poeme))
Corrigé de l'exercice 3
from pathlib import Path
def lire_config(chemin):
"""Lit un fichier 'cle = valeur' et renvoie un dictionnaire des réglages."""
reglages = {}
with open(chemin, "r", encoding="utf-8") as fichier:
for ligne in fichier:
ligne = ligne.strip()
if ligne == "" or ligne.startswith("#"):
continue # ligne vide ou commentaire
cle, valeur = ligne.split("=", 1)
reglages[cle.strip()] = valeur.strip()
return reglages
Path("config.txt").write_text(
"# Configuration du jeu\n"
"largeur = 800\n"
"hauteur=600\n"
"\n"
"# Son\n"
"volume = 75\n"
"pseudo = Joueur 1\n",
encoding="utf-8",
)
print(lire_config(Path("config.txt")))
Corrigé de l'exercice 4
import csv
from pathlib import Path
Path("notes.csv").write_text(
"nom;maths;francais;anglais\n"
"Alice;15;12;17\n"
"Bob;9;11;8\n"
"Chloé;18;16;14\n"
"David;10;9;12\n",
encoding="utf-8",
)
# Lecture et calcul
bulletin = []
with open(Path("notes.csv"), "r", encoding="utf-8", newline="") as fichier:
for ligne in csv.DictReader(fichier, delimiter=";"):
notes = [int(ligne[matiere]) for matiere in ["maths", "francais", "anglais"]]
moyenne = round(sum(notes) / len(notes), 2)
bulletin.append({
"nom": ligne["nom"],
"moyenne": moyenne,
"resultat": "admis" if moyenne >= 10 else "ajourné",
})
bulletin.sort(key=lambda eleve: eleve["moyenne"], reverse=True)
# Écriture
with open(Path("bulletin.csv"), "w", encoding="utf-8", newline="") as fichier:
ecrivain = csv.DictWriter(fichier, fieldnames=["nom", "moyenne", "resultat"], delimiter=";")
ecrivain.writeheader()
ecrivain.writerows(bulletin)
print(Path("bulletin.csv").read_text(encoding="utf-8"), end="")
Corrigé de l'exercice 5
import json
from pathlib import Path
PREFERENCES_PAR_DEFAUT = {"theme": "clair", "langue": "fr", "taille_police": 12}
def charger_preferences(chemin):
"""Renvoie les préférences du fichier, complétées par les valeurs par défaut."""
preferences = PREFERENCES_PAR_DEFAUT.copy()
if chemin.exists():
with open(chemin, "r", encoding="utf-8") as fichier:
preferences.update(json.load(fichier))
return preferences
def sauvegarder_preferences(preferences, chemin):
"""Enregistre les préférences au format JSON lisible."""
with open(chemin, "w", encoding="utf-8") as fichier:
json.dump(preferences, fichier, ensure_ascii=False, indent=4)
chemin = Path("preferences.json")
chemin.unlink(missing_ok=True) # on part d'une situation propre
preferences = charger_preferences(chemin)
print(f"Au départ : {preferences}")
preferences["theme"] = "sombre"
sauvegarder_preferences(preferences, chemin)
print(f"Après sauvegarde : {charger_preferences(chemin)}")
chemin.write_text('{"langue": "en"}', encoding="utf-8")
print(f"Fichier partiel : {charger_preferences(chemin)}")
Corrigé de l'exercice 6
from pathlib import Path
def inventaire_par_extension(dossier):
"""Renvoie {extension: [noms de fichiers triés]} pour les fichiers du dossier."""
inventaire = {}
for chemin in dossier.iterdir():
if chemin.is_file():
extension = chemin.suffix
if extension not in inventaire:
inventaire[extension] = []
inventaire[extension].append(chemin.name)
return {ext: sorted(noms) for ext, noms in inventaire.items()}
dossier = Path("inventaire")
dossier.mkdir(exist_ok=True)
for nom in ["rapport.txt", "photo.jpg", "budget.csv", "notes.txt", "vacances.jpg"]:
(dossier / nom).write_text(nom, encoding="utf-8")
inventaire = inventaire_par_extension(dossier)
for extension in sorted(inventaire):
print(f"{extension} : {', '.join(inventaire[extension])}")
Corrigé de l'exercice 7
import csv
import json
from pathlib import Path
def csv_vers_json(chemin_csv, chemin_json):
"""Convertit un CSV en JSON (liste de dictionnaires), les nombres entiers étant convertis."""
with open(chemin_csv, "r", encoding="utf-8", newline="") as fichier:
lignes = list(csv.DictReader(fichier))
donnees = []
for ligne in lignes:
donnees.append({cle: int(valeur) if valeur.isdigit() else valeur
for cle, valeur in ligne.items()})
with open(chemin_json, "w", encoding="utf-8") as fichier:
json.dump(donnees, fichier, ensure_ascii=False, indent=2)
def json_vers_csv(chemin_json, chemin_csv):
"""Convertit un JSON (liste de dictionnaires) en CSV."""
with open(chemin_json, "r", encoding="utf-8") as fichier:
donnees = json.load(fichier)
with open(chemin_csv, "w", encoding="utf-8", newline="") as fichier:
ecrivain = csv.DictWriter(fichier, fieldnames=list(donnees[0].keys()), lineterminator="\n")
ecrivain.writeheader()
ecrivain.writerows(donnees)
Path("villes.csv").write_text(
"ville,departement,habitants\n"
"Lyon,Rhône,522250\n"
"Lille,Nord,236710\n"
"Nantes,Loire-Atlantique,320732\n",
encoding="utf-8",
)
csv_vers_json(Path("villes.csv"), Path("villes.json"))
print(Path("villes.json").read_text(encoding="utf-8"))
with open(Path("villes.json"), "r", encoding="utf-8") as fichier:
villes = json.load(fichier)
print(f"Total habitants : {sum(v['habitants'] for v in villes)}")
json_vers_csv(Path("villes.json"), Path("villes_copie.csv"))
original = Path("villes.csv").read_text(encoding="utf-8")
copie = Path("villes_copie.csv").read_text(encoding="utf-8")
print(f"Copie identique : {original == copie}")
Corrigé de l'exercice 8
import csv
import json
from pathlib import Path
Path("ventes.csv").write_text(
"date,produit,categorie,quantite,prix_unitaire\n"
"2024-05-02,Clavier,Informatique,2,49.90\n"
"2024-05-02,Stylo,Papeterie,10,1.20\n"
"2024-05-03,Souris,Informatique,3,19.90\n"
"2024-05-03,Cahier,Papeterie,5,3.50\n"
"2024-05-04,Clavier,Informatique,1,49.90\n"
"2024-05-04,Casque,Audio,1,89.00\n",
encoding="utf-8",
)
# 1. Agrégation
ca_categories = {}
articles_categories = {}
ca_produits = {}
with open(Path("ventes.csv"), "r", encoding="utf-8", newline="") as fichier:
for vente in csv.DictReader(fichier):
quantite = int(vente["quantite"])
montant = quantite * float(vente["prix_unitaire"])
categorie = vente["categorie"]
produit = vente["produit"]
ca_categories[categorie] = ca_categories.get(categorie, 0) + montant
articles_categories[categorie] = articles_categories.get(categorie, 0) + quantite
ca_produits[produit] = ca_produits.get(produit, 0) + montant
# 2. Rapport CSV, trié par chiffre d'affaires décroissant
categories_triees = sorted(ca_categories, key=lambda c: ca_categories[c], reverse=True)
with open(Path("rapport_ventes.csv"), "w", encoding="utf-8", newline="") as fichier:
ecrivain = csv.DictWriter(fichier, fieldnames=["categorie", "articles", "chiffre_affaires"],
lineterminator="\n")
ecrivain.writeheader()
for categorie in categories_triees:
ecrivain.writerow({
"categorie": categorie,
"articles": articles_categories[categorie],
"chiffre_affaires": f"{ca_categories[categorie]:.2f}",
})
# 3. Rapport JSON
rapport = {
"total": round(sum(ca_categories.values()), 2),
"meilleur_produit": max(ca_produits, key=lambda p: ca_produits[p]),
"categories": {c: round(montant, 2) for c, montant in ca_categories.items()},
}
with open(Path("rapport_ventes.json"), "w", encoding="utf-8") as fichier:
json.dump(rapport, fichier, ensure_ascii=False, indent=2)
# 4. Affichage
print(Path("rapport_ventes.csv").read_text(encoding="utf-8"), end="")
print(Path("rapport_ventes.json").read_text(encoding="utf-8"))
Corrigé du mini-projet
import csv
import json
from pathlib import Path
FICHIER_CONTACTS = Path("contacts.json")
FICHIER_EXPORT = Path("contacts.csv")
CHAMPS = ["nom", "prenom", "telephone", "email"]
# ---------- Persistance ----------
def charger_contacts(chemin):
"""Renvoie la liste des contacts du fichier JSON (liste vide s'il n'existe pas)."""
if not chemin.exists():
return []
with open(chemin, "r", encoding="utf-8") as fichier:
return json.load(fichier)
def sauvegarder_contacts(contacts, chemin):
"""Enregistre la liste des contacts au format JSON lisible."""
with open(chemin, "w", encoding="utf-8") as fichier:
json.dump(contacts, fichier, ensure_ascii=False, indent=2)
def exporter_csv(contacts, chemin):
"""Exporte les contacts dans un CSV lisible par Excel."""
with open(chemin, "w", encoding="utf-8-sig", newline="") as fichier:
ecrivain = csv.DictWriter(fichier, fieldnames=CHAMPS, delimiter=";")
ecrivain.writeheader()
ecrivain.writerows(trier(contacts))
# ---------- Traitements (sans input ni print) ----------
def trier(contacts):
"""Renvoie une copie de la liste triée par nom puis prénom."""
return sorted(contacts, key=lambda c: (c["nom"], c["prenom"]))
def rechercher(contacts, texte):
"""Renvoie les contacts dont le nom, le prénom ou l'email contient texte."""
texte = texte.lower()
return [c for c in trier(contacts)
if texte in c["nom"].lower() or texte in c["prenom"].lower() or texte in c["email"].lower()]
# ---------- Interface ----------
def afficher_contacts(contacts):
"""Affiche les contacts dans un tableau numéroté."""
if not contacts:
print("Aucun contact.")
return
print(f"{'N°':<4}{'Nom':<12}{'Prénom':<10}{'Téléphone':<16}Email")
for numero, contact in enumerate(contacts, start=1):
print(f"{numero:<4}{contact['nom']:<12}{contact['prenom']:<10}"
f"{contact['telephone']:<16}{contact['email']}")
def demander_non_vide(message):
"""Redemande tant que la saisie est vide ; renvoie la saisie nettoyée."""
saisie = input(message).strip()
while saisie == "":
print("Ce champ est obligatoire.")
saisie = input(message).strip()
return saisie
def ajouter_contact(contacts):
"""Demande les informations d'un contact, l'ajoute et sauvegarde."""
contact = {
"nom": demander_non_vide("Nom : ").upper(),
"prenom": demander_non_vide("Prénom : ").capitalize(),
"telephone": input("Téléphone : ").strip(),
"email": input("Email : ").strip().lower(),
}
contacts.append(contact)
sauvegarder_contacts(contacts, FICHIER_CONTACTS)
print(f"{contact['prenom']} {contact['nom']} ajouté(e) et sauvegardé(e).")
def supprimer_contact(contacts):
"""Demande un numéro de contact, confirme, supprime et sauvegarde."""
tries = trier(contacts)
afficher_contacts(tries)
if not tries:
return
saisie = input("Numéro à supprimer : ").strip()
if not saisie.isdigit() or not 1 <= int(saisie) <= len(tries):
print("Numéro invalide.")
return
contact = tries[int(saisie) - 1]
confirmation = input(f"Supprimer {contact['prenom']} {contact['nom']} ? (o/n) ").strip().lower()
if confirmation == "o":
contacts.remove(contact)
sauvegarder_contacts(contacts, FICHIER_CONTACTS)
print("Contact supprimé.")
else:
print("Suppression annulée.")
def main():
"""Boucle principale du carnet de contacts."""
contacts = charger_contacts(FICHIER_CONTACTS)
while True:
print()
print(f"=== CARNET DE CONTACTS ({len(contacts)} contacts) ===")
print("1. Lister les contacts")
print("2. Ajouter un contact")
print("3. Rechercher")
print("4. Supprimer un contact")
print("5. Exporter en CSV")
print("6. Quitter")
choix = input("Ton choix : ").strip()
if choix == "1":
afficher_contacts(trier(contacts))
elif choix == "2":
ajouter_contact(contacts)
elif choix == "3":
texte = input("Rechercher : ").strip()
resultats = rechercher(contacts, texte)
print(f"{len(resultats)} résultat(s) :")
afficher_contacts(resultats)
elif choix == "4":
supprimer_contact(contacts)
elif choix == "5":
exporter_csv(contacts, FICHIER_EXPORT)
print(f"{len(contacts)} contact(s) exporté(s) dans {FICHIER_EXPORT}.")
elif choix == "6":
print("Au revoir !")
break
else:
print("Choix invalide.")
main()
=== CARNET DE CONTACTS (0 contacts) ===
1. Lister les contacts
2. Ajouter un contact
3. Rechercher
4. Supprimer un contact
5. Exporter en CSV
6. Quitter
Ton choix : 2
Nom : martin
Prénom : alice
Téléphone : 06 12 34 56 78
Email : Alice@Exemple.fr
Alice MARTIN ajouté(e) et sauvegardé(e).
=== CARNET DE CONTACTS (1 contacts) ===
1. Lister les contacts
2. Ajouter un contact
3. Rechercher
4. Supprimer un contact
5. Exporter en CSV
6. Quitter
Ton choix : 2
Nom : durand
Prénom :
Ce champ est obligatoire.
Prénom : bob
Téléphone : 07 98 76 54 32
Email : bob@mail.fr
Bob DURAND ajouté(e) et sauvegardé(e).
=== CARNET DE CONTACTS (2 contacts) ===
1. Lister les contacts
2. Ajouter un contact
3. Rechercher
4. Supprimer un contact
5. Exporter en CSV
6. Quitter
Ton choix : 1
N° Nom Prénom Téléphone Email
1 DURAND Bob 07 98 76 54 32 bob@mail.fr
2 MARTIN Alice 06 12 34 56 78 alice@exemple.fr
=== CARNET DE CONTACTS (2 contacts) ===
1. Lister les contacts
2. Ajouter un contact
3. Rechercher
4. Supprimer un contact
5. Exporter en CSV
6. Quitter
Ton choix : 6
Au revoir !
[
{
"nom": "MARTIN",
"prenom": "Alice",
"telephone": "06 12 34 56 78",
"email": "alice@exemple.fr"
},
{
"nom": "DURAND",
"prenom": "Bob",
"telephone": "07 98 76 54 32",
"email": "bob@mail.fr"
}
]
=== CARNET DE CONTACTS (2 contacts) ===
1. Lister les contacts
2. Ajouter un contact
3. Rechercher
4. Supprimer un contact
5. Exporter en CSV
6. Quitter
Ton choix : 3
Rechercher : ALI
1 résultat(s) :
N° Nom Prénom Téléphone Email
1 MARTIN Alice 06 12 34 56 78 alice@exemple.fr
=== CARNET DE CONTACTS (2 contacts) ===
1. Lister les contacts
2. Ajouter un contact
3. Rechercher
4. Supprimer un contact
5. Exporter en CSV
6. Quitter
Ton choix : 4
N° Nom Prénom Téléphone Email
1 DURAND Bob 07 98 76 54 32 bob@mail.fr
2 MARTIN Alice 06 12 34 56 78 alice@exemple.fr
Numéro à supprimer : 1
Supprimer Bob DURAND ? (o/n) o
Contact supprimé.
=== CARNET DE CONTACTS (1 contacts) ===
1. Lister les contacts
2. Ajouter un contact
3. Rechercher
4. Supprimer un contact
5. Exporter en CSV
6. Quitter
Ton choix : 5
1 contact(s) exporté(s) dans contacts.csv.
=== CARNET DE CONTACTS (1 contacts) ===
1. Lister les contacts
2. Ajouter un contact
3. Rechercher
4. Supprimer un contact
5. Exporter en CSV
6. Quitter
Ton choix : 6
Au revoir !
Récapitulatif¶
- Les fichiers rendent les données persistantes : elles survivent à la fin du programme.
pathlib.Pathmanipule les chemins de façon portable :/pour assembler,.name,.suffix,.parent,.exists(),.mkdir(parents=True, exist_ok=True),.iterdir(),.glob("*.txt").Path(__file__).parentdonne le dossier du script.- On ouvre toujours un fichier avec
with open(chemin, mode, encoding="utf-8") as fichier:; il est fermé automatiquement à la sortie du bloc. - Modes :
"r"lecture (erreur si absent),"w"écriture (écrase),"a"ajout à la fin,"x"création exclusive. - Lecture :
read()(tout),for ligne in fichier(ligne par ligne, penser àstrip()),read().splitlines()(liste propre). Raccourcis :Path.read_text()/Path.write_text(). - Écriture :
write()n'ajoute pas de\n;print(..., file=fichier)si. - CSV :
import csv, ouvrir avecnewline="";csv.reader/csv.writer(listes),csv.DictReader/csv.DictWriter(dictionnaires, avecfieldnamesetwriteheader()) ;delimiter=";"pour le format français. Toutes les valeurs lues sont des chaînes. - JSON :
import json;json.dump(donnees, fichier, ensure_ascii=False, indent=4)pour écrire,json.load(fichier)pour lire ;dumps/loadspour les chaînes. Types conservés, mais clés toujours converties en chaînes, et pas d'ensembles. - Schéma à retenir : une fonction
charger(chemin)(avec valeur par défaut si le fichier n'existe pas) et une fonctionsauvegarder(donnees, chemin).
Checklist de validation¶
- Je sais construire un chemin avec
Pathet l'opérateur/, et tester s'il existe. - J'ouvre mes fichiers avec
withetencoding="utf-8", sans exception. - Je connais la différence entre les modes
"r","w"et"a", et le danger de"w". - Je sais lire un fichier ligne par ligne en retirant les retours à la ligne.
- Je sais lire un CSV avec
DictReaderet convertir les valeurs en nombres. - Je sais écrire un CSV avec
DictWriter(avecnewline=""etwriteheader()). - Je sais sauvegarder et recharger une liste de dictionnaires en JSON lisible et avec accents.
- Mon carnet de contacts retrouve ses données quand je le relance.