Chapitre 719 minDébutant
Lire et écrire des fichiers : pathlib, CSV et JSON
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- manipuler chemins et dossiers avec pathlib ;
- lire et écrire un fichier texte dans le bon encodage ;
- lire et écrire un CSV, y compris un export Excel avec BOM ;
- convertir des données entre Python et JSON.
3 exercices corrigés
Lire et écrire des fichiers en Python repose sur trois outils de la bibliothèque standard : pathlib pour manipuler les chemins et parcourir les dossiers, open() pour lire et écrire du texte, et les modules csv et json pour les deux formats d'échange les plus courants. Ce chapitre les présente sur l'inventaire du parc de Sorbier Logistique, un fichier CSV exporté d'un tableur, avec les deux pièges qui font perdre une heure : l'encodage et la marque d'ordre des octets (BOM).
Les chemins avec pathlib
Un chemin se manipule avec un objet Path, du module pathlib, plutôt qu'avec des chaînes. L'opérateur / assemble les morceaux avec le bon séparateur, \ sous Windows ou / sous Linux :
>>> from pathlib import Path
>>> rapport = Path("rapports") / "2026-11" / "disques.csv"
>>> rapport
PosixPath('rapports/2026-11/disques.csv')
>>> rapport.name, rapport.stem, rapport.suffix
('disques.csv', 'disques', '.csv')
>>> rapport.parent
PosixPath('rapports/2026-11')
>>> rapport.with_suffix(".json")
PosixPath('rapports/2026-11/disques.json')
>>> rapport.exists()
False
>>> Path("inventaire.csv").exists()
True
>>> Path("inventaire.csv").stat().st_size
732
La console affiche PosixPath sous Linux et macOS, WindowsPath sous Windows : le même code marche partout. Un chemin relatif (rapports/…) part du dossier courant du terminal, pas du dossier du script. Pour un fichier rangé à côté du script, quel que soit l'endroit d'où on le lance, écris Path(__file__).parent / "inventaire.csv".
| Attribut ou méthode | Rôle |
|---|---|
p.name, p.stem, p.suffix | Nom complet, nom sans extension, extension |
p.parent | Le dossier qui contient p |
p.exists(), p.is_file(), p.is_dir() | Tests d'existence et de nature |
p.mkdir(parents=True, exist_ok=True) | Crée le dossier et ses parents, sans erreur s'il existe |
p.stat().st_size | Taille en octets |
p.read_text(encoding=…), p.write_text(…) | Lire ou écrire tout le fichier d'un coup |
p.glob("*.log"), p.rglob("*.log") | Fichiers du dossier, ou de toute l'arborescence |
p.unlink() | Supprime le fichier |
Lire et écrire un fichier texte
# texte.py : écrire, compléter et relire un fichier texte
from pathlib import Path
dossier = Path("rapports")
dossier.mkdir(exist_ok=True) # crée le dossier s'il n'existe pas
fichier = dossier / "controle.txt"
fichier.write_text("Contrôle du 8 novembre 2026\n", encoding="utf-8")
with open(fichier, "a", encoding="utf-8") as f: # "a" : ajout à la fin
f.write("srv-ad : OK\n")
f.write("srv-fichiers : ALERTE (87,5 %)\n")
with open(fichier, encoding="utf-8") as f: # lecture ligne par ligne
for numero, ligne in enumerate(f, start=1):
print(numero, ligne.strip())
print(len(fichier.read_text(encoding="utf-8").splitlines()), "lignes")
1 Contrôle du 8 novembre 2026
2 srv-ad : OK
3 srv-fichiers : ALERTE (87,5 %)
3 lignes
open(fichier, mode, encoding=…) ouvre un fichier ; le mode "r" (lecture, par défaut), "w" (écriture, qui écrase le fichier) ou "a" (ajout à la fin). Le bloc with ferme le fichier à la fin, même si une erreur survient au milieu : c'est la seule façon correcte d'ouvrir un fichier. Parcourir l'objet fichier donne les lignes une à une, avec leur \n final, que strip() retire. Cette lecture ligne à ligne ne charge jamais tout le fichier en mémoire : elle convient à un journal de plusieurs gigaoctets.
Parcourir un dossier
# dossiers.py : parcourir un dossier avec glob et rglob
from pathlib import Path
for journal in sorted(Path("journaux").glob("*.log")):
print(journal.name)
total = 0
for fichier in Path("partage").rglob("*"): # récursif : sous-dossiers compris
if fichier.is_file():
total += fichier.stat().st_size
print(f"partage : {total / 1024 ** 2:.1f} Mio")
for photo in sorted(Path("partage").rglob("*.jpg")):
print(photo.as_posix())
sauvegarde-2026-11-06.log
sauvegarde-2026-11-07.log
sauvegarde-2026-11-08.log
partage : 163.3 Mio
partage/Logistique/photos/quai-3.jpg
partage/Logistique/photos/quai-4.jpg
glob("*.log") cherche dans le dossier lui-même, rglob("*") dans toute l'arborescence. L'ordre des résultats dépend du système de fichiers : trie-les avec sorted() pour un affichage stable. as_posix() affiche le chemin avec des / sur tous les systèmes.
Lire un CSV avec DictReader
Un fichier CSV (Comma-Separated Values) est un tableau en texte : une ligne par enregistrement, des champs séparés par un délimiteur. Un tableur français utilise le point-virgule, car la virgule y sert de séparateur décimal. Voici l'inventaire du parc, inventaire.csv, enregistré depuis un tableur au format « CSV UTF-8 » :
nom;type;service;site;os;achat;fin_garantie;ip
PC-0012;Fixe;Comptabilité;Siège;Windows 11 24H2;2021-03-15;2025-03-15;192.168.10.71
PC-0015;Portable;Logistique;Mâcon;Windows 11 25H2;2024-09-02;2027-09-02;192.168.20.53
PC-0021;Fixe;Logistique;Siège;Windows 11 24H2;2023-01-10;2026-01-10;192.168.10.80
PC-0042;Portable;Direction;Siège;Windows 11 23H2;2020-06-30;2024-06-30;192.168.10.92
PC-0050;Fixe;Logistique;Mâcon;Windows 11 25H2;2025-02-17;2028-02-17;192.168.20.61
PC-0063;Portable;Commercial;Siège;Windows 11 25H2;2025-10-06;2028-10-06;192.168.10.97
PC-0071;Fixe;Comptabilité;Siège;Windows 11 24H2;2023-11-20;2026-11-20;192.168.10.104
srv-web;Serveur;Informatique;Siège;Debian 13;2025-08-11;2030-08-11;192.168.10.225
Première tentative :
import csv
with open("inventaire.csv", encoding="utf-8", newline="") as f:
for ligne in csv.DictReader(f, delimiter=";"):
print(ligne["nom"])
Traceback (most recent call last):
File "/home/etudiant/cours-python/bom.py", line 5, in <module>
print(ligne["nom"])
~~~~~^^^^^^^
KeyError: 'nom'
La colonne nom existe bien, pourtant Python ne la trouve pas. Le coupable est invisible : Excel commence ses fichiers « CSV UTF-8 » par trois octets spéciaux, la BOM (byte order mark), et le module csv lit la première colonne comme '\ufeffnom'. L'encodage utf-8-sig retire la BOM si elle est là et ne gêne pas si elle n'y est pas :
# lire_csv.py : lire l'inventaire et lister les postes hors garantie
import csv
AUJOURD_HUI = "2026-11-08" # format ISO : les dates se comparent comme du texte
with open("inventaire.csv", encoding="utf-8-sig", newline="") as f:
machines = list(csv.DictReader(f, delimiter=";"))
print(len(machines), "machines ; colonnes :", list(machines[0].keys()))
print(machines[0])
hors_garantie = [m for m in machines if m["fin_garantie"] < AUJOURD_HUI]
for m in hors_garantie:
print(f"{m['nom']:<9}{m['service']:<14}fin de garantie {m['fin_garantie']}")
8 machines ; colonnes : ['nom', 'type', 'service', 'site', 'os', 'achat', 'fin_garantie', 'ip']
{'nom': 'PC-0012', 'type': 'Fixe', 'service': 'Comptabilité', 'site': 'Siège', 'os': 'Windows 11 24H2', 'achat': '2021-03-15', 'fin_garantie': '2025-03-15', 'ip': '192.168.10.71'}
PC-0012 Comptabilité fin de garantie 2025-03-15
PC-0021 Logistique fin de garantie 2026-01-10
PC-0042 Direction fin de garantie 2024-06-30
csv.DictReader lit la première ligne comme en-tête et renvoie chaque ligne suivante sous forme de dictionnaire : on accède aux champs par leur nom, et le script résiste à une colonne ajoutée ou déplacée. Les valeurs sont toujours des chaînes : convertis avec int() ou float() avant de calculer. Les dates au format ISO (AAAA-MM-JJ) ont une propriété pratique : leur ordre alphabétique est l'ordre chronologique, donc "2025-03-15" < "2026-11-08" compare correctement deux dates. Pour des calculs de durée, le module datetime est plus sûr (chapitre 9).
Écrire un CSV
# ecrire_csv.py : écrire la liste des postes à renouveler dans un nouveau CSV
import csv
from pathlib import Path
AUJOURD_HUI = "2026-11-08"
with open("inventaire.csv", encoding="utf-8-sig", newline="") as f:
machines = list(csv.DictReader(f, delimiter=";"))
a_renouveler = [
{"nom": m["nom"], "service": m["service"], "fin_garantie": m["fin_garantie"]}
for m in machines
if m["fin_garantie"] < AUJOURD_HUI
]
with open("a-renouveler.csv", "w", encoding="utf-8-sig", newline="") as f:
ecrivain = csv.DictWriter(f, fieldnames=["nom", "service", "fin_garantie"], delimiter=";")
ecrivain.writeheader()
ecrivain.writerows(a_renouveler)
print(Path("a-renouveler.csv").read_text(encoding="utf-8-sig"), end="")
nom;service;fin_garantie
PC-0012;Comptabilité;2025-03-15
PC-0021;Logistique;2026-01-10
PC-0042;Direction;2024-06-30
csv.DictWriter écrit des dictionnaires, en suivant l'ordre de colonnes donné par fieldnames ; writeheader() écrit la ligne d'en-tête. Écrire en utf-8-sig remet une BOM : c'est ce qui permet à Excel d'ouvrir le fichier avec les bons accents d'un double-clic. Pour un fichier destiné à un autre programme, utf-8 suffit. Si tu n'as pas de dictionnaires, csv.writer écrit des listes avec writerow().
Ne fabrique jamais un CSV en collant des chaînes avec ";".join(...) : le jour où un champ contient un point-virgule ("Dupont; Martin"), le fichier est corrompu. Le module csv met ce champ entre guillemets automatiquement.
Lire et écrire du JSON
JSON (JavaScript Object Notation) est le format des API web et de nombreux fichiers de configuration. Ses objets correspondent aux dictionnaires Python, ses tableaux aux listes (JSON, XML et CSV).
# json_demo.py : écrire puis relire une configuration en JSON
import json
from pathlib import Path
config = {
"site": "Siège",
"seuils": {"alerte": 80, "critique": 90},
"serveurs": ["srv-ad", "srv-fichiers", "srv-journaux"],
"notifier": True,
"proxy": None,
}
Path("config.json").write_text(json.dumps(config, indent=2, ensure_ascii=False), encoding="utf-8")
print(Path("config.json").read_text(encoding="utf-8"))
relue = json.loads(Path("config.json").read_text(encoding="utf-8"))
print(relue["seuils"]["critique"], relue["serveurs"][-1], type(relue["notifier"]))
{
"site": "Siège",
"seuils": {
"alerte": 80,
"critique": 90
},
"serveurs": [
"srv-ad",
"srv-fichiers",
"srv-journaux"
],
"notifier": true,
"proxy": null
}
90 srv-journaux <class 'bool'>
| Fonction | Rôle |
|---|---|
json.dumps(objet) | Objet Python → texte JSON |
json.loads(texte) | Texte JSON → objet Python |
json.dump(objet, f) | Objet Python → fichier ouvert |
json.load(f) | Fichier ouvert → objet Python |
Les correspondances : dict ↔ objet, list ↔ tableau, str ↔ chaîne, int et float ↔ nombre, True/False ↔ true/false, None ↔ null. indent=2 rend le fichier lisible par un humain ; ensure_ascii=False écrit Siège tel quel au lieu de Si\u00e8ge. Un tuple devient un tableau, et un ensemble n'est pas sérialisable : convertis-le en liste.
| CSV | JSON | |
|---|---|---|
| Structure | Un tableau plat : lignes et colonnes | Des objets imbriqués, des listes dans des objets |
| Types | Tout est texte | Nombres, booléens, null |
| Lu par | Les tableurs, les imports en masse (comptes, inventaire) | Les API, les fichiers de configuration, les applications |
Exercices
Exercice 1 sur 3 Combien de machines par service ? Réussi À revoir
À partir de inventaire.csv, écris un script qui compte les machines de chaque service, les affiche par ordre alphabétique de service et écrit le résultat dans machines-par-service.csv (colonnes service et machines), lisible par Excel.
Voir la réponseMasquer la réponse de l'exercice 1
# ex_services.py : nombre de machines par service, écrit dans un CSV
import csv
from collections import Counter
with open("inventaire.csv", encoding="utf-8-sig", newline="") as f:
machines = list(csv.DictReader(f, delimiter=";"))
par_service = Counter(m["service"] for m in machines)
with open("machines-par-service.csv", "w", encoding="utf-8-sig", newline="") as f:
ecrivain = csv.writer(f, delimiter=";")
ecrivain.writerow(["service", "machines"])
for service, nombre in sorted(par_service.items()):
ecrivain.writerow([service, nombre])
print(f"{service:<14}{nombre}")
Commercial 1
Comptabilité 2
Direction 1
Informatique 1
Logistique 3
Counter reçoit directement un générateur des services. L'encodage utf-8-sig en lecture retire la BOM d'Excel, en écriture il en remet une pour qu'Excel affiche correctement Comptabilité.
Tu avais trouvé ?
Exercice 2 sur 3 Du CSV au JSON Réussi À revoir
Convertis inventaire.csv en inventaire.json, un objet dont les clés sont les sites et les valeurs la liste des machines du site (nom et adresse IP seulement). Affiche le nombre de machines par site, puis le JSON du site de Mâcon.
Voir la réponseMasquer la réponse de l'exercice 2
# ex_csv_json.py : convertir l'inventaire CSV en JSON, en regroupant par site
import csv
import json
from pathlib import Path
with open("inventaire.csv", encoding="utf-8-sig", newline="") as f:
machines = list(csv.DictReader(f, delimiter=";"))
par_site = {}
for m in machines:
par_site.setdefault(m["site"], []).append({"nom": m["nom"], "ip": m["ip"]})
texte = json.dumps(par_site, indent=2, ensure_ascii=False)
Path("inventaire.json").write_text(texte, encoding="utf-8")
print({site: len(liste) for site, liste in par_site.items()})
print(json.dumps(par_site["Mâcon"], indent=2, ensure_ascii=False))
{'Siège': 6, 'Mâcon': 2}
[
{
"nom": "PC-0015",
"ip": "192.168.20.53"
},
{
"nom": "PC-0050",
"ip": "192.168.20.61"
}
]
setdefault(cle, []) renvoie la liste de la clé, en la créant vide la première fois : c'est l'équivalent, sans import, du defaultdict(list) du chapitre 6.
Tu avais trouvé ?
Exercice 3 sur 3 Les plus gros fichiers Réussi À revoir
Le partage partage est presque plein. Écris un script qui affiche les trois plus gros fichiers de toute son arborescence, avec leur taille en Mio et leur chemin relatif au partage.
Voir la réponseMasquer la réponse de l'exercice 3
# ex_gros.py : les 3 plus gros fichiers d'un partage
from pathlib import Path
fichiers = [f for f in Path("partage").rglob("*") if f.is_file()]
fichiers.sort(key=lambda f: f.stat().st_size, reverse=True)
for f in fichiers[:3]:
print(f"{f.stat().st_size / 1024 ** 2:8.1f} Mio {f.relative_to('partage').as_posix()}")
150.0 Mio Direction/video-voeux.mp4
5.0 Mio Comptabilite/factures-octobre.pdf
3.2 Mio Logistique/photos/quai-3.jpg
On ne garde que les fichiers (is_file()), on trie par taille décroissante, et la tranche [:3] garde les trois premiers. relative_to('partage') retire le début du chemin. Sur un vrai partage de plusieurs centaines de milliers de fichiers, appeler stat() deux fois par fichier coûte cher : calcule la taille une fois, dans une liste de tuples (taille, chemin).
Tu avais trouvé ?