alpinux-wiki/docs/presentations/Python/07-fichiers-csv-json.md
2026-10-15 00:28:56 +02:00

2010 lines
70 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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](06-chaines-et-comprehensions.md) (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.DictReader` et `csv.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) ou `C:\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](00-installation-environnement.md)). 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](09-modules-bibliotheque-standard.md)) :
```python
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
```python
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__` :
```python
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
```python
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 (ici `essais`).
- `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 ») :
```python
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")))
```
```text
['a.txt', 'b.txt', 'c.csv', 'sous_dossier']
['a.txt', 'b.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` :
```python
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é.")
```
```text
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 :
```python
from pathlib import Path
with open(Path("bonjour.txt"), "r", encoding="latin-1") as fichier:
print(fichier.read())
```
```text
Bonjour le monde !
Deuxième ligne, avec des accents : é è ç ù.
```
**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 :
```python
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()`
```python
from pathlib import Path
with open(Path("fruits.txt"), "r", encoding="utf-8") as fichier:
contenu = fichier.read()
print(repr(contenu))
print(contenu)
```
```text
'pomme\nbanane\ncerise\n'
pomme
banane
cerise
```
`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.
```python
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}")
```
```text
1: pomme
2: banane
3: cerise
```
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) :
```python
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}")
```
```text
1: pomme
2: banane
3: cerise
```
#### 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 :
```python
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 :
```python
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 ?
```python
from pathlib import Path
with open(Path("inexistant.txt"), "r", encoding="utf-8") as fichier:
print(fichier.read())
```
```text
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()` :
```python
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](08-erreurs-et-debogage.md), 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.
```python
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.
```python
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` :
```python
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.
```python
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="")
```
```text
Rapport du jour
Total : 42 ventes
```
#### 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.
```python
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="")
```
```text
Démarrage
Connexion d'Alice
Connexion de Bob
```
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).
```python
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="")
```
```text
Alice Martin
Bob Durand
Chloé Petit
```
---
### 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).
```text
nom,age,ville
Alice,30,Lyon
Bob,25,Paris
```
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`
```python
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="")
```
```text
nom,note,classe
Alice,15.5,3A
"Dupont, Jean",12,3B
Chloé,18,3A
```
Points importants :
- **`newline=""`** est obligatoire pour les fichiers CSV : le module `csv` gè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`
```python
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)
```
```text
['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 :
1. Les guillemets autour de `Dupont, Jean` ont été correctement interprétés.
2. **Tout est du texte** : `'15.5'` et `'12'` sont des chaînes ! Pour calculer, il faut convertir avec `float()` ou `int()`.
Pour sauter la ligne d'en-têtes, on peut utiliser `next(lecteur)`, qui lit la ligne suivante :
```python
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 :
```python
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} €")
```
```text
2024-03-01 Courses 54.20
2024-03-03 Cinéma 11.50
Total : 65.70 €
```
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** :
```python
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)
```
```text
{'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 :
```python
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()` :
```python
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="")
```
```text
nom;prix;stock
Stylo;1.5;120
Cahier;3.2;45
Gomme;0.8;0
```
- L'ordre des colonnes dans le fichier est celui de `fieldnames`.
- Si un dictionnaire contient une clé absente de `fieldnames`, `DictWriter` provoque une erreur `ValueError` (on peut ajouter `extrasaction="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 :
```python
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="")
```
```text
nom;prix;stock
Stylo;1.5;120
Cahier;3.2;45
Gomme;0.8;0
Règle;2.1;30
```
> **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 avec `encoding="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 !
```json
{
"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`
```python
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"))
```
```text
{
"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 `\u00e9` pour `é` : le fichier reste valide, mais devient illisible pour un humain.
Compare :
```python
import json
print(json.dumps({"ville": "Besançon"}))
print(json.dumps({"ville": "Besançon"}, ensure_ascii=False))
```
```text
{"ville": "Besan\u00e7on"}
{"ville": "Besançon"}
```
(`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`
```python
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 :
```python
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 :
```python
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 :
```text
Scores chargés : {}
Scores sauvegardés : {'Inès': 150, 'Noé': 90}
```
Si tu relances le programme, les scores sont retrouvés et augmentés :
```text
Scores chargés : {'Inès': 150, 'Noé': 90}
Scores sauvegardés : {'Inès': 300, 'Noé': 180}
```
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).
```python
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 :
```python
import json
json.dumps({"tags": {"python", "json"}})
```
```text
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"`
```python
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())
```
```text
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"`
```python
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`
```python
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
```python
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
```python
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"]
```
```text
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
```python
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
```python
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 ⭐
1. Écris dans `table7.txt` la table de multiplication de 7, de 1 à 10, une opération par ligne (`7 x 1 = 7`).
2. 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 :
```text
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 :
```text
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 :
```text
{'lignes': 3, 'mots': 25, 'caracteres': 145}
```
### 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 `#` :
```text
# Configuration du jeu
largeur = 800
hauteur=600
# Son
volume = 75
pseudo = Joueur 1
```
É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 `=`.
```python
print(lire_config(Path("config.txt")))
```
```text
{'largeur': '800', 'hauteur': '600', 'volume': '75', 'pseudo': 'Joueur 1'}
```
### Exercice 4 — Bulletin à partir d'un CSV ⭐⭐
Le fichier `notes.csv` (séparateur `;`) contient une ligne par élève :
```text
nom;maths;francais;anglais
Alice;15;12;17
Bob;9;11;8
Chloé;18;16;14
David;10;9;12
```
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 :
```text
nom;moyenne;resultat
Chloé;16.0;admis
Alice;14.67;admis
David;10.33;admis
Bob;9.33;ajourné
```
### 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.
```text
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 :
```text
.csv : budget.csv
.jpg : photo.jpg, vacances.jpg
.txt : notes.txt, rapport.txt
```
### 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` :
```text
ville,departement,habitants
Lyon,Rhône,522250
Lille,Nord,236710
Nantes,Loire-Atlantique,320732
```
Puis reconvertis le JSON en `villes_copie.csv` et vérifie que le contenu est identique à l'original.
Sortie attendue :
```text
[
{
"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 :
```text
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 :
```text
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 :
```json
{"nom": "Martin", "prenom": "Alice", "telephone": "06 12 34 56 78", "email": "alice@exemple.fr"}
```
Le carnet entier est une **liste** de ces dictionnaires. Le programme affiche un menu en boucle :
```text
=== 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 `;`, encodage `utf-8-sig` pour Excel) avec `DictWriter`.
- **Quitter** : message d'au revoir.
### Étapes suggérées
1. Écris `charger_contacts(chemin)` et `sauvegarder_contacts(contacts, chemin)` (sur le modèle des scores de la section 7) et teste-les seules.
2. Écris `afficher_contacts(contacts)` avec un tableau aligné par f-strings et `sorted(..., key=lambda c: (c["nom"], c["prenom"]))`.
3. Écris `demander_non_vide(message)` puis `ajouter_contact(contacts)`.
4. Écris `rechercher(contacts, texte)` qui **renvoie** la liste des résultats (sans `print`).
5. Écris `supprimer_contact(contacts)` en vérifiant que le numéro saisi est valide (`isdigit()` et dans les bornes).
6. Écris `exporter_csv(contacts, chemin)`.
7. 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.json` avant chaque écriture.
- Gérer un fichier JSON corrompu avec `try/except` ([chapitre 08](08-erreurs-et-debogage.md)), puis transformer les contacts en objets ([chapitre 10](10-poo-classes-objets.md)).
---
## Corrigés
<details>
<summary>Corrigé de l'exercice 1</summary>
```python
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}")
```
Sortie :
```text
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
```
À l'écriture, la f-string se termine par `\n` pour que chaque opération soit sur sa propre ligne. À la lecture, on boucle directement sur le fichier avec `enumerate` pour avoir le numéro de ligne, on retire le `\n` avec `strip()`, puis on récupère le résultat en coupant sur `=` : `"7 x 2 = 14".split("=")` donne `['7 x 2 ', ' 14']`, et `int()` accepte les espaces autour du nombre.
</details>
<details>
<summary>Corrigé de l'exercice 2</summary>
```python
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))
```
Sortie :
```text
{'lignes': 3, 'mots': 25, 'caracteres': 145}
```
On lit ligne par ligne (efficace même pour un très gros fichier) et on met à jour trois compteurs. `rstrip("\n")` ne retire que le retour à la ligne, pas les éventuels espaces en fin de ligne, qui sont de vrais caractères. `split()` sans argument découpe sur les espaces : « l'aube, » compte pour un seul mot. Les accents comptent pour un caractère chacun, grâce au décodage UTF-8.
</details>
<details>
<summary>Corrigé de l'exercice 3</summary>
```python
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")))
```
Sortie :
```text
{'largeur': '800', 'hauteur': '600', 'volume': '75', 'pseudo': 'Joueur 1'}
```
`continue` (chapitre 03) passe directement à la ligne suivante pour les lignes à ignorer. `split("=", 1)` coupe au **premier** `=` seulement : une valeur pourrait contenir un `=` (par exemple une formule). Les valeurs restent des chaînes ; c'est au programme qui les utilise de les convertir (`int(reglages["largeur"])`). Une ligne sans `=` provoquerait une erreur au déballage : on apprendra à la gérer proprement au chapitre 08.
</details>
<details>
<summary>Corrigé de l'exercice 4</summary>
```python
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="")
```
Sortie :
```text
nom;moyenne;resultat
Chloé;16.0;admis
Alice;14.67;admis
David;10.33;admis
Bob;9.33;ajourné
```
`DictReader` permet d'écrire `ligne["maths"]` plutôt que `ligne[1]`. Une compréhension convertit les trois notes en entiers. On construit une liste de dictionnaires (les lignes du futur fichier), qu'on trie avec une `lambda` avant de la donner à `DictWriter`. Le même `delimiter=";"` est utilisé en lecture et en écriture.
</details>
<details>
<summary>Corrigé de l'exercice 5</summary>
```python
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)}")
```
Sortie :
```text
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}
```
L'astuce est de partir d'une **copie** des valeurs par défaut, puis de la mettre à jour avec `update()`, une méthode des dictionnaires qui ajoute ou remplace les paires d'un autre dictionnaire. Sans `.copy()`, on modifierait la constante elle-même (piège de la référence, chapitre 04). `unlink()` supprime un fichier ; `missing_ok=True` évite l'erreur s'il n'existe pas.
</details>
<details>
<summary>Corrigé de l'exercice 6</summary>
```python
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])}")
```
Sortie :
```text
.csv : budget.csv
.jpg : photo.jpg, vacances.jpg
.txt : notes.txt, rapport.txt
```
`iterdir()` parcourt le contenu du dossier ; `is_file()` écarte les éventuels sous-dossiers. `suffix` donne l'extension et `name` le nom complet. Comme l'ordre de `iterdir()` n'est pas garanti, on trie les noms (compréhension de dictionnaire) et les extensions à l'affichage. C'est la base d'un script qui range automatiquement un dossier de téléchargements (cf. [chapitre 14](14-specialisations.md)).
</details>
<details>
<summary>Corrigé de l'exercice 7</summary>
```python
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}")
```
Sortie :
```text
[
{
"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
```
La compréhension de dictionnaire avec `int(valeur) if valeur.isdigit() else valeur` convertit seulement les valeurs numériques : dans le JSON, `522250` apparaît sans guillemets, c'est un vrai nombre, et la somme fonctionne directement après rechargement. Pour l'inverse, les colonnes sont les clés du premier dictionnaire.
Un détail : par défaut, `csv.writer` termine les lignes par `\r\n` (la convention officielle du format CSV). Pour obtenir un fichier **strictement** identique à l'original, qui utilise `\n`, on a précisé `lineterminator="\n"`. Sans cette option, le contenu serait le même pour un tableur, mais la comparaison de chaînes renverrait `False`.
</details>
<details>
<summary>Corrigé de l'exercice 8</summary>
```python
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"))
```
Sortie :
```text
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
}
}
```
On réutilise le schéma « compter avec un dictionnaire et `get` » (chapitre 04), cette fois pour additionner des montants. Trois dictionnaires d'agrégation suffisent. Dans le CSV, le chiffre d'affaires est formaté avec `:.2f` pour toujours afficher 2 décimales (`89.00`) ; dans le JSON, on garde de vrais nombres arrondis avec `round` (le JSON n'affiche pas les zéros inutiles). `round` est important : sans lui, les calculs en virgule flottante pourraient donner des valeurs comme `209.39999999999998`.
</details>
<details>
<summary>Corrigé du mini-projet</summary>
```python
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()
```
Exemple de première session, en partant sans fichier `contacts.json` (saisies : `2`, `martin`, `alice`, `06 12 34 56 78`, `Alice@Exemple.fr`, `2`, `durand`, une ligne vide, `bob`, `07 98 76 54 32`, `bob@mail.fr`, `1`, `6`) :
```text
=== 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 !
```
Le fichier `contacts.json` contient alors :
```json
[
{
"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"
}
]
```
Deuxième session, en relançant le programme (saisies : `3`, `ALI`, `4`, `1`, `o`, `5`, `6`) : les deux contacts ont bien été retrouvés.
```text
=== 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 !
```
**Points clés de la solution :**
- Le code est organisé en trois couches : **persistance** (fichiers), **traitements** (calculs purs, sans `input` ni `print`), **interface** (dialogue avec l'utilisateur). Cette séparation facilitera les tests ([chapitre 13](13-tests-pytest-bonnes-pratiques.md)) et un éventuel passage à une interface web ([chapitre 14](14-specialisations.md)).
- Le fichier est **sauvegardé après chaque modification** : même si le programme est fermé brutalement, rien n'est perdu.
- `charger_contacts` gère l'absence de fichier au premier lancement grâce à `exists()`.
- `ensure_ascii=False` et `indent=2` rendent `contacts.json` lisible et modifiable à la main.
- La suppression se fait sur la liste **triée** affichée (pour que le numéro corresponde à ce que voit l'utilisateur), puis `contacts.remove(contact)` retire ce même dictionnaire de la liste d'origine : les deux listes contiennent des références vers les mêmes dictionnaires (chapitre 04).
- `not 1 <= int(saisie) <= len(tries)` vérifie les bornes ; grâce à l'évaluation paresseuse de `or`, `int(saisie)` n'est appelé que si `isdigit()` est vrai.
- L'export utilise `utf-8-sig` et `;` pour qu'Excel affiche correctement les accents et les colonnes.
</details>
---
## Récapitulatif
- Les fichiers rendent les données **persistantes** : elles survivent à la fin du programme.
- `pathlib.Path` manipule les chemins de façon portable : `/` pour assembler, `.name`, `.suffix`, `.parent`, `.exists()`, `.mkdir(parents=True, exist_ok=True)`, `.iterdir()`, `.glob("*.txt")`. `Path(__file__).parent` donne 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 avec `newline=""` ; `csv.reader` / `csv.writer` (listes), `csv.DictReader` / `csv.DictWriter` (dictionnaires, avec `fieldnames` et `writeheader()`) ; `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`/`loads` pour 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 fonction `sauvegarder(donnees, chemin)`.
### Checklist de validation
- [ ] Je sais construire un chemin avec `Path` et l'opérateur `/`, et tester s'il existe.
- [ ] J'ouvre mes fichiers avec `with` et `encoding="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 `DictReader` et convertir les valeurs en nombres.
- [ ] Je sais écrire un CSV avec `DictWriter` (avec `newline=""` et `writeheader()`).
- [ ] 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.
---
[← Chapitre précédent](06-chaines-et-comprehensions.md) | [Sommaire](README.md) | [Chapitre suivant →](08-erreurs-et-debogage.md)