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 :

ModeSignifieSi le fichier existeS'il n'existe pas
"r"read, lectureon lit💥 FileNotFoundError
"w"write, écritureil est VIDÉ immédiatementil est créé
"a"append, ajouton écrit À LA SUITEil 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 :

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éflexeswith 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 :

  1. 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.
  2. Au-dessus : le fichier .py, la ligne, et le open() souligné.
  3. 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 (cd dedans 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é.

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

14. Questions de révision

  1. Que se passe-t-il exactement à l'instant où open("x.txt", "w") est exécuté, avant tout write() ?
  2. Pourquoi with est-il obligatoire — quelles sont les deux choses concrètes qui peuvent mal tourner avec un fichier jamais fermé ?
  3. Pourquoi met-on encoding="utf-8" aussi en LECTURE ?
  4. Que contient l'élément lines[0] après lines = f.readlines() sur un fichier commençant par pain ? Précisément ?
  5. Quelle est la différence de comportement entre "w" et "a" si le fichier existe ? Et s'il n'existe pas ?
  6. Pourquoi f.read() appelé deux fois renvoie-t-il une chaîne vide la deuxième fois ?
  7. Un fichier utf-8 relu en cp1252 ne lève aucune erreur — pourquoi est-ce PIRE qu'un traceback ?
  8. Décris le motif sûr pour trier les lignes d'un fichier existant.

15. Checklist de compréhension

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.