Leçon 05-1 — Lire et écrire des fichiers
1. Objectif
À la fin de cette leçon tu sauras : ouvrir un fichier texte avec with open(...), le lire en entier ou ligne par ligne, écrire et ajouter du texte dedans, choisir le bon mode ("r", "w", "a"), nettoyer les \n avec strip(), et expliquer pourquoi on écrit toujours encoding="utf-8" — surtout sous Windows.
2. Pourquoi c'est important
Jusqu'ici, tout ce que tes programmes fabriquaient mourait avec eux : tu fermes le terminal, la liste de courses disparaît, le score s'évapore. Les fichiers sont la première forme de persistance : des données qui survivent à l'arrêt du programme. Configuration, journaux, sauvegardes, exports, données d'entrée — la quasi-totalité des vrais programmes lit ou écrit des fichiers. C'est aussi la leçon qui transforme tes mini-projets en vrais outils : la todo-list qui se souvient de tes tâches, le quiz qui charge ses questions depuis un fichier.
3. Explication simple
Un fichier texte, c'est une feuille de papier rangée dans le disque dur. Pour travailler dessus, il faut trois étapes : le sortir du tiroir (ouvrir), lire ou écrire, le ranger (fermer). En Python, with open(...) fait tout ça proprement :
with open("notes.txt", "r", encoding="utf-8") as f:
content = f.read()
print(content)
Se lit : « ouvre notes.txt en lecture ("r"), appelle-le f, et referme-le automatiquement à la fin du bloc indenté ». f.read() renvoie tout le contenu sous forme d'une seule chaîne de caractères.
Les trois modes à connaître :
| Mode | Signifie | Si le fichier existe | S'il n'existe pas |
|---|---|---|---|
"r" | read, lecture | on lit | 💥 FileNotFoundError |
"w" | write, écriture | il est VIDÉ immédiatement | il est créé |
"a" | append, ajout | on écrit À LA SUITE | il est créé |
Et encoding="utf-8" dit à Python comment traduire les caractères en octets. On le met à chaque open(), sans exception. Pourquoi ? Section 4.
4. Explication approfondie
a) Pourquoi with est obligatoire ici. Tu peux techniquement écrire f = open(...) puis f.close() toi-même. Mais si une erreur explose entre les deux, close() n'est jamais exécuté et le fichier reste ouvert. Deux conséquences concrètes : ce que tu as « écrit » peut ne jamais atteindre le disque (Python garde un tampon en mémoire et ne le vide qu'à la fermeture), et sous Windows un fichier ouvert est verrouillé — impossible de le supprimer ou de le renommer tant qu'un programme le tient. with garantit la fermeture dans tous les cas, même en cas d'erreur. Dans ce cours, open() sans with est considéré comme une faute.
b) Pourquoi encoding="utf-8" partout. Un fichier ne contient pas des caractères, il contient des octets. L'encodage est la table de traduction caractères ↔ octets. UTF-8 est le standard mondial… mais si tu ne précises rien, Python utilise l'encodage par défaut du système — et sous Windows en français, c'est souvent cp1252, PAS utf-8. Résultat : le même programme écrit un fichier chez toi, le lit de travers ailleurs, ou casse les accents. Les deux symptômes classiques :
caféécrit en utf-8, relu sans préciser l'encodage → affichécafé(le mojibake : pas d'erreur, juste du charabia silencieux) ;- l'inverse →
UnicodeDecodeError(section 7).
La règle du cours : chaque open() porte encoding="utf-8", en lecture comme en écriture. C'est trois mots de plus qui t'épargnent des heures.
c) "w" détruit AVANT d'écrire. Ouvrir en mode "w" vide le fichier à l'ouverture, immédiatement, même si tu n'écris rien ensuite ou si ton programme plante à la ligne suivante. C'est LA perte de données classique du niveau. Réflexe : avant chaque "w", demande-toi « ai-je le droit d'écraser ce fichier ? ». Pour ajouter sans détruire : "a".
d) Les lignes gardent leur \n. Quand tu lis ligne par ligne, chaque ligne se termine par le caractère invisible \n (saut de ligne). Si tu l'oublies, tes affichages ont des lignes vides en trop et tes comparaisons échouent ("pain\n" == "pain" est faux). Le remède : line.strip(), qui retire les espaces et sauts de ligne aux deux bouts.
Difficulté honnête : la mécanique de cette leçon est simple. Ce qui demande de la pratique, c'est les réflexes — with toujours, encoding toujours, méfiance envers "w", strip() sur chaque ligne lue. Quatre réflexes, une semaine d'exercices, et c'est acquis à vie.
5. Exemples commentés
Exemple 1 — écrire un fichier
with open("courses.txt", "w", encoding="utf-8") as f:
f.write("pain\n") # write() n'ajoute PAS de saut de ligne :
f.write("lait\n") # c'est toi qui mets les \n
f.write("oeufs\n")
Après exécution, courses.txt existe (dans le dossier courant) et contient trois lignes. Note : contrairement à print(), f.write() n'ajoute jamais de \n tout seul.
Exemple 2 — lire tout d'un coup
with open("courses.txt", "r", encoding="utf-8") as f:
content = f.read() # UNE chaîne avec tout le fichier
print(content)
Sortie :
pain
lait
oeufs
content vaut "pain\nlait\noeufs\n" — les \n sont dedans, c'est eux qui font les sauts de ligne à l'affichage. ("r" est le mode par défaut : on peut l'omettre, mais l'écrire rend l'intention lisible.)
Exemple 3 — lire ligne par ligne (la bonne façon)
with open("courses.txt", encoding="utf-8") as f:
for line in f: # le fichier s'itère comme une liste
print("-", line.strip()) # strip() retire le \n final
Sortie :
- pain
- lait
- oeufs
C'est la façon idiomatique de parcourir un fichier : pas besoin de tout charger en mémoire, une ligne à la fois. Sans strip(), chaque print ajouterait SON saut de ligne à celui de la ligne → une ligne vide sur deux.
Exemple 4 — readlines() : le fichier vers une liste
with open("courses.txt", encoding="utf-8") as f:
lines = f.readlines()
print(lines)
Sortie :
['pain\n', 'lait\n', 'oeufs\n']
Chaque élément garde son \n. Pour une liste propre, on construit en bouclant (⏪ niveau 03) :
items = []
for line in lines:
items.append(line.strip())
print(items) # ['pain', 'lait', 'oeufs']
readlines() est utile quand tu as besoin de la liste entière (trier, compter, indexer).
Exemple 5 — ajouter sans détruire : le mode "a"
with open("courses.txt", "a", encoding="utf-8") as f:
f.write("beurre\n") # écrit À LA SUITE du contenu existant
Relance ce programme trois fois : beurre apparaît trois fois. "a" ajoute toujours à la fin — c'est le mode des journaux (logs) : on empile, on n'écrase jamais.
Exemple 6 — le cycle complet : lire, transformer, réécrire
# read everything FIRST, close the file, THEN rewrite
items = []
with open("courses.txt", encoding="utf-8") as f:
for line in f:
items.append(line.strip())
items.sort() # transform in memory
with open("courses.txt", "w", encoding="utf-8") as f:
for item in items:
f.write(item + "\n")
Le motif à retenir : lire → fermer → transformer en mémoire → réécrire. Deux blocs with séparés. Jamais lire et écrire le même fichier dans le même bloc — tu écraserais ce que tu es en train de lire.
6. Erreurs fréquentes
1) Ouvrir en "w" un fichier qu'on voulait compléter
with open("journal.txt", "w", encoding="utf-8") as f: # w = tout est déjà perdu
f.write("entrée du jour\n")
Aucune erreur, aucun avertissement : le journal des six derniers mois vient d'être remplacé par une ligne. Correction : "a" pour ajouter. Retiens-le comme un slogan : "w" comme wipe.
2) Oublier encoding="utf-8"
with open("notes.txt", "w") as f: # encodage du système : cp1252 ici
f.write("café crème\n")
Ça « marche »… jusqu'au jour où un autre programme (ou le même script sur une autre machine) relit le fichier en utf-8 : UnicodeDecodeError ou café crème. Le pire est que le bug se déclenche loin de sa cause. Correction : encoding="utf-8" dans chaque open(), lecture ET écriture.
3) Oublier le \n dans write()
with open("out.txt", "w", encoding="utf-8") as f:
f.write("ligne 1")
f.write("ligne 2")
Contenu du fichier : ligne 1ligne 2 — une seule ligne collée. write() écrit exactement ce qu'on lui donne, rien de plus. Correction : f.write("ligne 1\n").
4) Comparer une ligne sans strip()
with open("courses.txt", encoding="utf-8") as f:
for line in f:
if line == "lait": # toujours faux : line vaut "lait\n"
print("trouvé !")
Aucun message d'erreur, « trouvé ! » ne s'affiche jamais. Le \n invisible fait échouer la comparaison. Correction : if line.strip() == "lait":.
5) Lire deux fois de suite
with open("courses.txt", encoding="utf-8") as f:
print(f.read()) # lit tout, le "curseur" est à la fin
print(f.read()) # affiche : rien — chaîne vide !
Un fichier ouvert a un curseur de lecture qui avance. Après un read() complet, il est à la fin : le deuxième read() renvoie "". Correction : lire UNE fois dans une variable, réutiliser la variable.
6) Travailler hors du bloc with
with open("courses.txt", encoding="utf-8") as f:
pass
content = f.read() # ValueError: I/O operation on closed file.
À la sortie du bloc, f est fermé — c'est tout l'intérêt de with. Tout ce qui touche au fichier doit être dans le bloc indenté ; ce qui n'en a pas besoin (calculs, affichages) peut vivre après.
7. Lire les messages d'erreur
L'erreur emblématique de la leçon est FileNotFoundError :
Traceback (most recent call last):
File "C:\Users\jtron\Claude\learnpython\lessons\level-05-fichiers-exceptions-modules\exercices\mes-reponses\ex-05-1-a.py", line 1, in <module>
with open("notes.txt", encoding="utf-8") as f:
~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: 'notes.txt'
Lecture de bas en haut :
- Dernière ligne :
FileNotFoundError— Python n'a pas trouvénotes.txt. Le message répète le chemin cherché : lis-le attentivement, c'est souvent là que se cache la faute de frappe. - Au-dessus : le fichier
.py, la ligne, et leopen()souligné. - Trois causes possibles, dans l'ordre de probabilité : (1) tu n'es pas dans le dossier que tu crois — le chemin relatif part du dossier courant du terminal, pas du dossier du script (c'est LE piège, la leçon 05-2 lui est en partie consacrée) ; (2) faute de frappe dans le nom ; (3) le fichier n'a réellement jamais été créé.
Deuxième erreur typique, UnicodeDecodeError :
Traceback (most recent call last):
File "C:\Users\jtron\Claude\learnpython\lessons\level-05-fichiers-exceptions-modules\exercices\mes-reponses\ex-05-1-b.py", line 2, in <module>
print(f.read())
~~~~~~^^
File "<frozen codecs>", line 325, in decode
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9 in position 3: unexpected end of data
Traduction : « j'ai essayé de décoder le fichier en utf-8, et l'octet 0xe9 n'est pas de l'utf-8 valide ». 0xe9, c'est é encodé en… cp1252 : le fichier a été écrit avec un autre encodage que celui de lecture. La ligne <frozen codecs> est du code interne de Python — ignore-la, ton code à toi est dans la ligne au-dessus. Remède : ouvrir avec le bon encodage (si le fichier vient d'un vieil outil Windows, essaie encoding="cp1252"), puis le réécrire en utf-8 pour ne plus jamais y penser.
À noter : l'erreur inverse (utf-8 relu en cp1252) ne lève aucune exception — elle affiche café. Un traceback est un cadeau : le mojibake silencieux est plus sournois.
8. Exercices — faciles
Règles : nouveau fichier par exercice dans
lessons/level-05-fichiers-exceptions-modules/exercices/mes-reponses/, nomméex-05-1-a.py, etc. Lance chaque script depuis ce dossier (cddedans d'abord) — le pourquoi est dans la leçon 05-2. Prédis le comportement AVANT d'exécuter.
a) Écris un programme qui crée courses.txt contenant trois articles (un par ligne), puis — dans un second bloc with — le relit et affiche son contenu. Contraintes : with et encoding="utf-8" partout, aucun article tapé deux fois dans le code.
b) Sans exécuter : ton fichier courses.txt contient 3 lignes. Écris sur papier ce que renvoie exactement f.readlines() (la liste complète, avec les guillemets et les caractères invisibles). Vérifie ensuite avec print.
c) Écris ajoute.py qui ajoute UNE ligne beurre à courses.txt sans toucher au reste. Lance-le deux fois. Combien de lignes contient le fichier maintenant, et pourquoi ? Écris la réponse en commentaire dans le script.
9. Exercices — moyens
a) Écris un programme qui affiche courses.txt numéroté :
1. pain
2. lait
3. oeufs
Contraintes : enumerate(..., start=1) (⏪ niveau 03), aucune ligne vide parasite dans la sortie, et le programme doit marcher quel que soit le nombre de lignes du fichier.
b) Un compteur de lancements persistant : compteur.txt contient un nombre (crée-le à la main avec 0 dedans). Écris compteur.py qui lit le nombre, l'augmente de 1, le réécrit, et affiche Ce programme a été lancé N fois.. Lance-le cinq fois. Piège à éviter : ce que renvoie read() n'est pas un int, et il traîne peut-être un \n. (⏩ Le cas « le fichier n'existe pas encore » sera géré proprement en 05-3.)
c) Écris nettoie.py : lis sale.txt (crée-le avec des lignes vides, des espaces en début/fin de lignes), et écris dans propre.txt les mêmes lignes nettoyées, sans les lignes vides. Le fichier d'origine ne doit pas être modifié. Vérifie en ouvrant les deux fichiers dans VS Code.
10. Exercices — difficiles
a) Écris inverse.py qui inverse l'ordre des lignes de courses.txt dans le même fichier (la première devient la dernière). Le programme doit être sûr : si tu le relances, le fichier est re-inversé, jamais corrompu ni vidé.
- Indice 1 : peut-on lire et écrire le même fichier dans un seul bloc
with? Relis l'exemple 6. - Indice 2 : il te faut TOUT le contenu en mémoire avant d'ouvrir en
"w". - Indice 3 :
readlines()puis la technique de renversement de liste du niveau 03 (lines.reverse()ou slice[::-1]), puis un secondwithen"w"qui réécrit.
11. Mini-projet lié — « journal de bord persistant »
Écris journal.py : à chaque lancement, le programme demande Qu'as-tu appris aujourd'hui ? et ajoute la réponse dans journal.txt, précédée d'un numéro d'entrée (Entrée 4 : ...). Puis il réaffiche tout le journal.
Contraintes : le journal survit aux relances (mode réfléchi !) ; le numéro d'entrée est calculé en comptant les lignes existantes, pas stocké en dur ; with + encoding="utf-8" partout. Durée cible : 20–30 min. Amorce : crée journal.txt vide à la main (le cas « fichier absent » est l'affaire de la leçon 05-3).
Extensions : une option « chercher un mot » qui n'affiche que les entrées contenant le mot (in, niveau 03) ; ⏩ horodater avec datetime (module standard, importé — mécanique vue en 05-4).
Pour aller plus loin dans le même esprit : le quiz interactif charge ses questions depuis un fichier — tu peux déjà en faire les étapes 1 à 3, et tu auras tout le bagage fichier après cette leçon (sa version JSON est guidée pas à pas dans la fiche).
12. Correction / méthode de correction
Les solutions détaillées sont dans solutions/serie-05-1-solutions.md.
Méthode : (1) finis TOUS les exercices avant d'ouvrir les solutions ; (2) compare le raisonnement, pas seulement le code ; (3) vérifie particulièrement tes réflexes : chaque open() a-t-il son with et son encoding ? (4) pour chaque écart, une ligne de journal : « je pensais X, en fait Y » ; (5) refais de tête l'exercice raté le lendemain.
13. À retenir
with open(path, mode, encoding="utf-8") as f:— la SEULE façon d'ouvrir un fichier dans ce cours."r"lit,"a"ajoute,"w"écrase immédiatement (w comme wipe).encoding="utf-8"à chaqueopen(): le défaut Windows (cp1252) casse les accents, souvent sans erreur.for line in f:— la façon idiomatique de lire ligne par ligne.- Chaque ligne lue garde son
\n→strip()avant de comparer ou afficher. write()n'ajoute pas de\n: c'est toi qui les écris.- Modifier un fichier = lire → fermer → transformer en mémoire → réécrire (deux
withséparés). - Le curseur de lecture avance : un fichier se lit UNE fois par ouverture.
14. Questions de révision
- Que se passe-t-il exactement à l'instant où
open("x.txt", "w")est exécuté, avant toutwrite()? - Pourquoi
withest-il obligatoire — quelles sont les deux choses concrètes qui peuvent mal tourner avec un fichier jamais fermé ? - Pourquoi met-on
encoding="utf-8"aussi en LECTURE ? - Que contient l'élément
lines[0]aprèslines = f.readlines()sur un fichier commençant parpain? Précisément ? - Quelle est la différence de comportement entre
"w"et"a"si le fichier existe ? Et s'il n'existe pas ? - Pourquoi
f.read()appelé deux fois renvoie-t-il une chaîne vide la deuxième fois ? - Un fichier utf-8 relu en cp1252 ne lève aucune erreur — pourquoi est-ce PIRE qu'un traceback ?
- Décris le motif sûr pour trier les lignes d'un fichier existant.
15. Checklist de compréhension
- Je sais écrire puis relire un fichier avec
withetencoding="utf-8"sans modèle sous les yeux. - Je peux réciter ce que fait chaque mode
r/w/a, fichier présent ou absent. - Je mets
strip()sur les lignes lues sans qu'on me le rappelle. - Je peux expliquer pourquoi le compteur (exercice 9-b) a besoin de
int()ET destrip(). - Je peux provoquer volontairement un
FileNotFoundErroret unUnicodeDecodeError, et lire les deux tracebacks. - J'ai fait le mini-projet journal et il survit aux relances.
Si une case reste vide : refais les exercices de la section correspondante demain, AVANT d'ouvrir la leçon suivante.
16. Commit conseillé
git add lessons/level-05-fichiers-exceptions-modules/exercices/mes-reponses/
git commit -m "exercises: complete lesson 05-1 reading and writing files"
Puis, si c'est ta fin de session : entrée de journal + git push.