Solutions — série 05-1 (lire et écrire des fichiers)
Rappel de la règle d'or : on n'ouvre ce fichier qu'après avoir terminé (ou sérieusement séché sur) la série. Compare le raisonnement, pas seulement le code : une solution différente qui marche est souvent valable. Et vérifie systématiquement tes deux réflexes : chaque
open()a sonwithet sonencoding="utf-8".
Exercice facile a — écrire puis relire courses.txt
Raisonnement — Deux opérations distinctes (écrire, puis lire) = deux blocs with séparés. La contrainte « aucun article tapé deux fois » impose de stocker les articles UNE fois dans une liste (⏪ niveau 03) et de boucler dessus pour écrire.
Solution
# ex-05-1-a.py
items = ["pain", "lait", "oeufs"]
with open("courses.txt", "w", encoding="utf-8") as f:
for item in items:
f.write(item + "\n") # write() does not add the newline itself
with open("courses.txt", encoding="utf-8") as f:
print(f.read())
Sortie :
pain
lait
oeufs
(La ligne vide finale vient du dernier \n du fichier + celui de print — normal.)
Pourquoi ça marche — Le premier with ouvre en "w" : le fichier est créé (ou vidé s'il existait) et chaque write y ajoute un article suivi de son \n. À la sortie du bloc, le fichier est fermé et son contenu garanti sur le disque ; le second with peut alors le relire tranquillement en "r" (mode par défaut).
Erreur classique sur cet exercice — Tout faire dans UN seul with ouvert en "w" : f.read() échoue alors avec io.UnsupportedOperation: not readable — un fichier ouvert en écriture ne se lit pas. Autre classique : f.write(item) sans \n → les trois articles collés sur une ligne (painlaitoeufs), sans aucune erreur.
Variante plus difficile — Réécris l'écriture en UNE ligne avec "\n".join(items) + "\n" (⏪ join, niveau 03) et un seul write. Compare les deux versions : même fichier au final ?
Exercice facile b — prédire readlines()
Raisonnement — Question de représentation exacte : readlines() découpe le fichier AUX sauts de ligne mais garde chaque \n dans son élément. Trois lignes → trois chaînes, chacune terminée par \n.
Solution — Pour courses.txt contenant pain, lait, oeufs :
with open("courses.txt", encoding="utf-8") as f:
print(f.readlines())
['pain\n', 'lait\n', 'oeufs\n']
Pourquoi ça marche — Le fichier est UNE suite de caractères : "pain\nlait\noeufs\n". readlines() coupe après chaque \n sans le retirer — c'est documenté et constant. C'est pour ça que le nettoyage (strip()) est TON travail, à la lecture.
Erreur classique sur cet exercice — Prédire ['pain', 'lait', 'oeufs'] (oubli des \n) — c'est précisément l'intuition fausse que l'exercice corrige. Plus subtil : si le fichier ne se termine PAS par un saut de ligne, le dernier élément n'a pas de \n (['pain\n', 'lait\n', 'oeufs']) — ne compte jamais dessus, strip() gère les deux cas.
Variante plus difficile — Prédis f.read().splitlines() sur le même fichier. (Réponse : ['pain', 'lait', 'oeufs'] — splitlines() retire les \n, lui. Deux outils proches, deux résultats différents : sache lequel tu utilises.)
Exercice facile c — ajouter avec "a"
Raisonnement — « Ajouter sans toucher au reste » = mode "a", qui écrit toujours à la fin. La question du double lancement teste si on a compris que "a" empile.
Solution
# ex-05-1-c.py (ajoute.py)
with open("courses.txt", "a", encoding="utf-8") as f:
f.write("beurre\n")
# After two runs the file has 5 lines: the 3 original items plus
# one "beurre" per run — "a" always appends, it never replaces.
Pourquoi ça marche — En mode "a", le curseur d'écriture est placé À LA FIN du fichier à l'ouverture : chaque lancement ajoute exactement une ligne. Après deux lancements : pain, lait, oeufs, beurre, beurre — 5 lignes.
Erreur classique sur cet exercice — Utiliser "w" : après deux lancements le fichier contient UNE ligne (beurre) — les courses ont été détruites au premier lancement, silencieusement. Si tu l'as fait : c'est la leçon la plus utile de la série, note-la dans ton journal.
Variante plus difficile — Fais en sorte que beurre ne soit ajouté que s'il n'y est pas déjà : lis d'abord les lignes dans une liste nettoyée (strip() sur chacune), teste "beurre" in items, et n'ajoute que si absent. Relançable à volonté = « idempotent » — une propriété que tu recroiseras.
Exercice moyen a — affichage numéroté
Raisonnement — Trois contraintes qui désignent trois outils : numéroter → enumerate(..., start=1) ; pas de ligne vide parasite → strip() sur chaque ligne ; nombre de lignes quelconque → itérer sur le fichier, rien de codé en dur.
Solution
# ex-05-1-d.py
with open("courses.txt", encoding="utf-8") as f:
for number, line in enumerate(f, start=1):
print(f"{number}. {line.strip()}")
Sortie :
1. pain
2. lait
3. oeufs
Pourquoi ça marche — enumerate fonctionne sur tout ce qui s'itère — y compris un fichier ouvert : il fournit des paires (numéro, ligne). Chaque line arrive avec son \n ; strip() le retire, et print remet LE SIEN : exactement un saut de ligne par article.
Erreur classique sur cet exercice — Oublier strip() : la sortie a une ligne vide sur deux (le \n de la ligne + celui de print). Ou tenir le compteur à la main (number = 0 puis number += 1) : ça marche, mais enumerate existe pour ça — c'était le piège « recopier le niveau 02 au lieu d'utiliser le niveau 03 ».
Variante plus difficile — Aligne les numéros sur deux caractères pour un fichier de 10+ lignes (f"{number:>2}. ..." — le mini-langage de format des f-strings ; explore aussi :< et :^).
Exercice moyen b — le compteur persistant
Raisonnement — Le motif complet de la persistance en trois temps : lire l'état (fichier → int), le modifier (en mémoire), le réécrire (mode "w", légitime ici : on VEUT remplacer l'ancienne valeur). Les deux pièges annoncés sont à la lecture : read() renvoie une chaîne, potentiellement avec \n.
Solution
# ex-05-1-e.py (compteur.py)
with open("compteur.txt", encoding="utf-8") as f:
count = int(f.read().strip()) # strip THEN int: "3\n" -> "3" -> 3
count += 1
with open("compteur.txt", "w", encoding="utf-8") as f:
f.write(str(count) + "\n") # write() only accepts strings
print(f"Ce programme a été lancé {count} fois.")
Cinq lancements après un compteur.txt contenant 0 :
Ce programme a été lancé 1 fois.
...
Ce programme a été lancé 5 fois.
Pourquoi ça marche — La chaîne d'opérations f.read().strip() puis int(...) neutralise les deux pièges dans le bon ordre. À l'écriture, le symétrique : write() refuse un int (TypeError: write() argument must be str), d'où str(count). Le "w" est correct : l'ancien nombre DOIT être remplacé.
Erreur classique sur cet exercice — int(f.read()) marche tant que le fichier ne contient pas d'espaces parasites… puis casse un jour — mets le strip() d'office. Et surtout : rouvrir en "a" « pour ne pas écraser » → le fichier contient 0\n1\n2\n… et la lecture suivante plante. Ici, écraser est le comportement voulu — savoir QUAND "w" est le bon mode fait partie de la leçon.
Variante plus difficile — Fais résister ton compteur à un compteur.txt contenant douze : affiche un message clair au lieu d'un traceback. Avec tes outils actuels c'est laborieux — c'est exactement le travail de la leçon 05-3 (try/except ValueError). Reviens faire cette variante après.
Exercice moyen c — le nettoyeur
Raisonnement — Le motif « lire → transformer en mémoire → écrire AILLEURS » (le fichier d'origine est préservé, contrainte de l'énoncé). Deux transformations par ligne : strip(), puis filtrer les lignes devenues vides.
Solution
# ex-05-1-f.py (nettoie.py)
lines = []
with open("sale.txt", encoding="utf-8") as f:
for line in f:
lines.append(line.strip()) # clean every line
with open("propre.txt", "w", encoding="utf-8") as f:
for line in lines:
if line != "": # drop the now-empty lines
f.write(line + "\n")
Avec un sale.txt contenant pain blanc, une ligne vide, lait, deux lignes vides, oeufs — propre.txt contient :
pain blanc
lait
oeufs
Pourquoi ça marche — strip() normalise chaque ligne (espaces ET \n) ; une ligne « vide » ("\n", " \n") devient "", ce que le if écarte. sale.txt n'est ouvert qu'en lecture : il est intact — vérifiable dans VS Code.
Erreur classique sur cet exercice — Filtrer AVANT de nettoyer : if line != "" sur la ligne brute ne retire rien (une ligne « vide » brute vaut "\n", pas ""). L'ordre strip-puis-filtre est significatif. Autre version : if line: — équivalente à if line != "": (chaîne vide = fausse) et parfaitement acceptable si tu sais POURQUOI.
Variante plus difficile — Ajoute la déduplication : chaque ligne n'est écrite qu'à sa première apparition (⏪ set du niveau 03 : seen = set(), if line not in seen). Attention à préserver l'ORDRE — pourquoi set(lines) seul ne suffit-il pas ?
Exercice difficile a — inverser le fichier sur place
Raisonnement — On ne peut pas lire et écrire le même fichier dans un même bloc : ouvrir en "w" viderait ce qu'on s'apprête à lire. Donc trois temps stricts : TOUT lire en mémoire (le with de lecture se ferme), inverser la liste, réécrire en "w". La sûreté au relancement vient gratuitement : inverser deux fois = ordre initial, et chaque étape est complète avant la suivante.
Solution
# ex-05-1-g.py (inverse.py)
with open("courses.txt", encoding="utf-8") as f:
lines = f.readlines() # full content, \n included
lines.reverse() # in-place reversal (⏪ niveau 03)
with open("courses.txt", "w", encoding="utf-8") as f:
for line in lines:
f.write(line) # each line still has its \n
Premier lancement : oeufs, lait, pain. Deuxième : pain, lait, oeufs. Jamais corrompu.
Pourquoi ça marche — readlines() garde les \n : en réécrivant les lignes telles quelles, la structure du fichier est préservée sans avoir à gérer les sauts de ligne. Le moment le plus risqué est l'ouverture en "w" — mais à cet instant, tout le contenu est déjà en sécurité dans lines.
Erreur classique sur cet exercice — Le « tout-en-un » : with open("courses.txt", "w", ...) puis tenter de lire → le fichier est DÉJÀ vide, données perdues (recrée-le, ce n'est qu'un exercice — mais retiens la brûlure). Piège secondaire : lines = lines.reverse() — reverse() renvoie None (⏪ le piège sort() du niveau 03, même famille).
Variante plus difficile — (1) Version slice : remplace reverse() par lines[::-1] — où la liste inversée vit-elle, et pourquoi faut-il l'assigner ? (2) Version robuste : écris d'abord dans courses.tmp, puis renomme sur l'original (⏩ Path.replace(), leçon 05-2) — c'est ainsi que les vrais programmes évitent de perdre des données si le courant saute entre les deux with.
Mini-projet — « journal de bord persistant »
Raisonnement — Trois besoins, trois morceaux : compter les entrées existantes (lecture), ajouter la nouvelle (mode "a" — le journal EMPILE, jamais "w"), tout réafficher (relecture). Le numéro d'entrée est calculé = nombre de lignes + 1 : le fichier est la seule source de vérité, aucun compteur parallèle à maintenir.
Solution
# journal.py
with open("journal.txt", encoding="utf-8") as f:
entry_count = len(f.readlines()) # existing entries
answer = input("Qu'as-tu appris aujourd'hui ? ").strip()
with open("journal.txt", "a", encoding="utf-8") as f:
f.write(f"Entrée {entry_count + 1} : {answer}\n")
print("--- Journal complet ---")
with open("journal.txt", encoding="utf-8") as f:
for line in f:
print(line.strip())
Session (2e lancement) :
Qu'as-tu appris aujourd'hui ? le mode w écrase sans prévenir
--- Journal complet ---
Entrée 1 : strip retire le \n
Entrée 2 : le mode w écrase sans prévenir
Pourquoi ça marche — Une entrée = une ligne, donc len(f.readlines()) EST le nombre d'entrées — le numéro se déduit du fichier au lieu d'être stocké (moins d'état = moins de bugs). Le mode "a" garantit que l'historique n'est jamais touché. Trois with distincts, chacun une responsabilité : compter, ajouter, afficher.
Erreur classique sur cet exercice — Le "w" réflexe : le journal ne contient plus que l'entrée du jour — relis la section 6 de la leçon, erreur 1. Plus fin : écrire le numéro d'entrée dans une variable persistée à part (un compteur.txt d'à côté) — double source de vérité, désynchronisation garantie le jour où tu édites le journal à la main.
Variante plus difficile — (1) La recherche : demande un mot et n'affiche que les entrées le contenant, insensible à la casse (mot.lower() in line.lower()). (2) Une entrée multi-lignes ? Réfléchis : avec « une entrée = une ligne », comment stocker un retour à la ligne saisi par l'utilisateur ? (C'est une vraie question de format de données — et une des raisons d'être du JSON, ⏩ niveau 09.) (3) Après 05-3 : gère le premier lancement sans créer le fichier à la main.