Travailler avec des fichiers texte : open, with, lecture et écriture

Objectifs de la leçon
  • Ouvrir un fichier dans le mode adapté — r, w ou a — et comprendre ce que fait chaque mode au contenu existant
  • Utiliser with pour que le fichier soit toujours fermé, même en cas d’erreur
  • Lire un fichier en entier, ligne par ligne, et y écrire et ajouter
  • Construire un chemin vers un fichier relatif à l’emplacement du script avec pathlib, et gérer FileNotFoundError

Qu’allons-nous apprendre ?

  • Ouvrir un fichier avec open et les modes d’ouverture.
  • Pourquoi un fichier doit être fermé, et comment with le fait automatiquement.
  • La lecture : le fichier entier, ligne par ligne.
  • L’écriture et l’ajout.
  • Un chemin robuste, qui ne dépend pas de l’endroit d’où l’on lance (promis dans le thème sur l’ordinateur, le système d’exploitation et le réseau !).

Pourquoi en avons-nous besoin ?

Le thème sur l’ordinateur, le système d’exploitation et le réseau : « tout ce qui est en RAM disparaît à la fin du programme ». Les fichiers sont le moyen de base pour enregistrer des informations sur le stockage : réglages, rapports, historique, données d’import et d’export. Le compteur qui revenait toujours à 1 dans memory_test.py ? Aujourd’hui, nous allons le faire survivre d’une exécution à l’autre.

Et dans le projet de POO, vous enregistrerez toutes les informations du système dans un fichier.

Explication à partir des bases

Ouvrir un fichier

open(path, mode) demande au système d’exploitation (un appel système !) l’accès au fichier, et renvoie un objet fichier à travers lequel on lit et on écrit.

Les modes d’ouverture

ModeNomSi le fichier existeS’il n’existe pasActions
"r"Read (par défaut)L’ouvre en lectureFileNotFoundErrorLecture seule
"w"WriteSupprime tout le contenu !Le créeÉcriture
"a"AppendGarde le contenu, et écrit à la finLe créeÉcriture à la fin
Le mode w supprime !

open("data.txt", "w") vide le fichier dès son ouverture, avant même que vous ayez écrit un seul caractère. Si vous vouliez ajouter, les informations précédentes sont déjà supprimées. Pour ajouter, utilisez toujours "a".

Pourquoi faut-il fermer un fichier ?

  • L’écriture dans un fichier est d’abord accumulée dans un tampon (buffer) en mémoire, et réellement écrite sur le disque seulement à la fermeture du fichier ou quand le tampon est plein. Sans fermeture, une partie des informations risque de ne pas être enregistrée.
  • Le système d’exploitation limite le nombre de fichiers qu’un processus peut garder ouverts. Les fichiers non fermés « fuient ».
  • Sous Windows, un fichier ouvert est parfois verrouillé, et d’autres programmes ne peuvent pas y accéder.

with — la fermeture automatique

with open("notes.txt", "r") as file:
    content = file.read()
# here the file is already closed - always

with garantit que le fichier est fermé dès que l’on quitte le bloc : à la fin normale, lors d’un return, et même quand une erreur est lancée. Vous vous souvenez de finally du thème exceptions et journalisation ? with fait exactement cela, en version courte. Ouvrez toujours les fichiers avec with.

Analogie : un livre à la bibliothèque

open = prendre un livre sur l’étagère. close = le remettre. Sans with, il faut penser à le remettre, et si quelqu’un vous appelle en plein milieu (une erreur), le livre reste sur la table. with = un bibliothécaire qui remet le livre à sa place dès que vous vous levez de la table, quelle que soit la raison.

Les façons de lire

FaçonRenvoieQuand
file.read()Une seule chaîne avec tout le fichierPetits fichiers
file.readlines()Une liste de lignes (chacune avec \n à la fin)Quand on a besoin de toutes les lignes sous forme de liste
for line in file:Ligne par ligneLa façon recommandée. Fonctionne même pour des fichiers énormes, car elle ne charge pas tout en mémoire

\n est le caractère de saut de ligne (newline). Chaque ligne du fichier se termine par lui. line.strip() le supprime (ainsi que les espaces aux extrémités).

L’écriture

file.write(text) écrit exactement le texte, sans passer automatiquement à la ligne (contrairement à print). Vous voulez une nouvelle ligne ? Ajoutez "\n" vous-même. Et write n’accepte qu’une chaîne : convertissez d’abord les nombres avec str().

Un chemin robuste avec pathlib

Dans le thème sur l’ordinateur, le système d’exploitation et le réseau, nous avons vu qu’un chemin relatif se mesure depuis le dossier d’où l’on a lancé. La solution professionnelle : construire le chemin par rapport à l’emplacement du fichier .py lui-même.

from pathlib import Path

BASE_DIR = Path(__file__).parent      # the folder this .py file is in
DATA_FILE = BASE_DIR / "data.txt"     # "/" joins path parts, on every OS
  • __file__ — une variable intégrée : le chemin du fichier actuel.
  • Path(...).parent — le dossier qui le contient.
  • / entre un Path et une chaîne — joint correctement les parties du chemin sur tout système d’exploitation (\ sous Windows, / sur Mac).

Maintenant, DATA_FILE pointe vers le même fichier quel que soit l’endroit d’où vous lancez.

Syntaxe

from pathlib import Path

path = Path(__file__).parent / "notes.txt"

# write (creates or overwrites)
with open(path, "w", encoding="utf-8") as file:
    file.write("first line\n")

# append (adds to the end)
with open(path, "a", encoding="utf-8") as file:
    file.write("another line\n")

# read everything
with open(path, "r", encoding="utf-8") as file:
    content = file.read()

# read line by line
with open(path, "r", encoding="utf-8") as file:
    for line in file:
        print(line.strip())

path.exists()     # True / False

encoding="utf-8" — indiquez-le toujours. Sans lui, sous Windows l’encodage par défaut est différent, et un texte non anglais (comme l’hébreu ou les caractères accentués) ou des emoji seront écrits de façon illisible ou lanceront une UnicodeDecodeError.

Premier exemple de code

week-2/files/diary.py :

from pathlib import Path

DIARY_FILE = Path(__file__).parent / "diary.txt"

with open(DIARY_FILE, "w", encoding="utf-8") as file:
    file.write("Day 1: started the course\n")
    file.write("Day 2: learned the terminal\n")

with open(DIARY_FILE, "a", encoding="utf-8") as file:
    file.write("Day 3: networks!\n")

with open(DIARY_FILE, "r", encoding="utf-8") as file:
    content = file.read()

print(content)
print("File size in characters:", len(content))

Sortie :

Day 1: started the course
Day 2: learned the terminal
Day 3: networks!

File size in characters: 71

Et le fichier diary.txt a été créé dans le dossier. Ouvrez-le dans VS Code.

Analyse du code ligne par ligne

LigneExplication
Path(__file__).parent / "diary.txt"Un chemin complet vers le fichier à côté du script. Fonctionne depuis n’importe quel dossier.
open(DIARY_FILE, "w", ...)Mode écriture : crée le fichier, ou le vide s’il existe.
file.write("...\n")Écrit du texte. Le \n crée un saut de ligne.
La fin du premier bloc withLe fichier est fermé et le contenu est écrit sur le disque.
open(..., "a", ...)Mode ajout : le contenu existant est conservé, et l’écriture se fait à la fin.
file.read()Tout le contenu sous forme d’une seule chaîne.
Une ligne vide dans la sortieLe fichier se termine par \n, et print ajoute son propre \n.

Essayez : relancez-le. Le fichier a-t-il grandi ? Non, car le mode "w" au début du script le vide à chaque exécution. Changez la première ouverture en "a" et lancez-le deux fois. Que se passe-t-il ?

Un autre exemple

Un compteur d’exécutions qui survit vraiment (le problème du thème sur l’ordinateur, le système d’exploitation et le réseau !) + le traitement d’un fichier de données ligne par ligne.

week-2/files/run_counter.py :

from pathlib import Path

COUNTER_FILE = Path(__file__).parent / "counter.txt"

def read_count():
    if not COUNTER_FILE.exists():
        return 0
    with open(COUNTER_FILE, "r", encoding="utf-8") as file:
        return int(file.read().strip())

def save_count(count):
    with open(COUNTER_FILE, "w", encoding="utf-8") as file:
        file.write(str(count))

def main():
    count = read_count() + 1
    save_count(count)
    print("This program has run", count, "times")

if __name__ == "__main__":
    main()

Trois exécutions :

This program has run 1 times
This program has run 2 times
This program has run 3 times

Maintenant ça fonctionne, car le nombre est enregistré sur le stockage entre les exécutions, et pas seulement en RAM.

Le traitement d’un fichier de notes. Créez week-2/files/grades.txt :

Dana,90
Avi,72
Noa,abc
Tom,85

Rina,64

week-2/files/grades_report.py :

from pathlib import Path

GRADES_FILE = Path(__file__).parent / "grades.txt"
REPORT_FILE = Path(__file__).parent / "report.txt"

def load_grades(path):
    grades = {}
    with open(path, "r", encoding="utf-8") as file:
        for line_number, line in enumerate(file, start=1):
            line = line.strip()
            if line == "":
                continue
            name, grade_text = line.split(",")
            try:
                grades[name] = int(grade_text)
            except ValueError:
                print("Skipping line", line_number, "- bad grade:", repr(grade_text))
    return grades

def write_report(grades, path):
    with open(path, "w", encoding="utf-8") as file:
        for name, grade in grades.items():
            status = "PASS" if grade >= 65 else "FAIL"
            file.write(name + ": " + str(grade) + " " + status + "\n")
        average = sum(grades.values()) / len(grades)
        file.write("Average: " + str(round(average, 1)) + "\n")

def main():
    grades = load_grades(GRADES_FILE)
    write_report(grades, REPORT_FILE)
    print("Report written to", REPORT_FILE.name)

if __name__ == "__main__":
    main()

Sortie à l’écran :

Skipping line 3 - bad grade: 'abc'
Report written to report.txt

report.txt :

Dana: 90 PASS
Avi: 72 PASS
Tom: 85 PASS
Rina: 64 FAIL
Average: 77.8

Analyse :

  • for line_number, line in enumerate(file, start=1) — parcourt le fichier ligne par ligne, avec un numéro de ligne, pour signaler où se trouve le problème.
  • line.strip() — supprime le \n. Sans lui, int("90\n") fonctionne en fait, mais le nom arriverait avec des espaces en trop.
  • if line == "": continue — sauter les lignes vides.
  • name, grade_text = line.split(",") — découpage selon la virgule, et Unpacking en deux variables.
  • "PASS" if grade >= 65 else "FAIL" — une expression conditionnelle (Conditional Expression) : un if/else court sur une ligne, qui renvoie l’une de deux valeurs.
  • REPORT_FILE.name — seulement le nom du fichier, sans le chemin complet.
  • Path n’est pas qu’« un chemin ». Il a des méthodes utiles comme .exists() et .name.

Erreur courante

Erreur 1 : "w" au lieu de "a" — et des données supprimées. Nous l’avons vu plus haut. Avant chaque open(..., "w"), demandez-vous : « est-ce que la suppression du contenu précédent me convient ? »

Erreur 2 : FileNotFoundError à cause d’un chemin relatif.

with open("grades.txt") as file:

Ne fonctionne que quand on lance depuis le même dossier. Depuis le bouton Run de VS Code, ou depuis un autre dossier :

FileNotFoundError: [Errno 2] No such file or directory: 'grades.txt'

La correction : Path(__file__).parent / "grades.txt". Et pour les fichiers qui peuvent ne pas exister, vérifiez .exists() ou attrapez FileNotFoundError.

Erreur 3 : écrire un nombre avec write.

file.write(42)
TypeError: write() argument must be str, not int

La correction : file.write(str(42)).

Erreur 4 : oublier \n.

file.write("line one")
file.write("line two")

Dans le fichier : line oneline two, sur une seule ligne. write n’ajoute pas de saut de ligne.

Erreur 5 : lire deux fois depuis le même fichier ouvert.

with open(path, encoding="utf-8") as file:
    first = file.read()
    second = file.read()
print(repr(second))   # ''

Le fichier « se souvient » de l’endroit où il lit (une position de lecture). Après read(), la position est à la fin, donc la seconde lecture renvoie une chaîne vide.

Exercices

Les exercices n’utilisent que ce qui a été vu dans cette leçon et les précédentes. Essayez d’abord seul, et n’ouvrez la solution qu’ensuite.

Exercice 1: Écrire et lire

Écrivez deux lignes dans le fichier diary.txt : Day 1: started et Day 2: networks. Puis lisez le fichier et affichez son contenu.

Résultat attendu :

Day 1: started
Day 2: networks
Afficher la solution
with open("diary.txt", "w", encoding="utf-8") as file:
    file.write("Day 1: started\n")
    file.write("Day 2: networks\n")

with open("diary.txt", "r", encoding="utf-8") as file:
    print(file.read(), end="")
Day 1: started
Day 2: networks
  • with ferme le fichier automatiquement, même en cas d’erreur.
  • write n’ajoute pas de saut de ligne tout seul — il faut \n.
  • end="" — parce que le contenu se termine déjà par \n.

Exercice 2: w face à a

with open("log.txt", "w", encoding="utf-8") as file:
    file.write("first\n")
with open("log.txt", "w", encoding="utf-8") as file:
    file.write("second\n")
with open("log.txt", "a", encoding="utf-8") as file:
    file.write("third\n")

Que contiendra le fichier à la fin ? Expliquez.

Afficher la solution
with open("log.txt", "w", encoding="utf-8") as file:
    file.write("first\n")
with open("log.txt", "w", encoding="utf-8") as file:
    file.write("second\n")
with open("log.txt", "a", encoding="utf-8") as file:
    file.write("third\n")

with open("log.txt", "r", encoding="utf-8") as file:
    print(file.read(), end="")
second
third
  • "w" supprime le fichier et repart de zéro — c’est pourquoi first a été supprimé quand le fichier a été ouvert en "w" la deuxième fois.
  • "a" ajoute à la fin, donc third a été ajouté après second.

Exercice 3: Ligne après ligne

Dans le fichier grades.txt, chaque ligne est name,grade. Écrivez un code qui lit le fichier ligne après ligne, et affiche chaque nom avec sa note. Sautez les lignes vides.

Créez le fichier ainsi :

with open("grades.txt", "w", encoding="utf-8") as file:
    file.write("dana,90\n\navi,75\nnoa,88\n")

Résultat attendu :

dana 90
avi 75
noa 88
Afficher la solution
with open("grades.txt", "w", encoding="utf-8") as file:
    file.write("dana,90\n\navi,75\nnoa,88\n")

with open("grades.txt", "r", encoding="utf-8") as file:
    for line in file:
        line = line.strip()
        if not line:
            continue
        name, grade_text = line.split(",")
        print(name, int(grade_text))
dana 90
avi 75
noa 88
  • for line in file — parcourt le fichier ligne après ligne.
  • strip() — supprime le \n et les espaces aux extrémités.
  • split(",") — découpe la ligne, et l’Unpacking la place dans deux variables.

Exercice 4: Une entrée défectueuse

Le même fichier de notes, mais il contient maintenant une ligne défectueuse. Lisez le fichier : une ligne dont la note n’est pas un nombre — sautez-la et affichez un message avec le numéro de ligne. À la fin, affichez la moyenne des notes valides.

with open("grades.txt", "w", encoding="utf-8") as file:
    file.write("dana,90\navi,abc\nnoa,80\n")

Résultat attendu :

line 2 skipped: avi,abc
Average: 85.0
Afficher la solution
with open("grades.txt", "w", encoding="utf-8") as file:
    file.write("dana,90\navi,abc\nnoa,80\n")

grades = []
with open("grades.txt", "r", encoding="utf-8") as file:
    for number, line in enumerate(file, start=1):
        line = line.strip()
        name, grade_text = line.split(",")
        try:
            grades.append(int(grade_text))
        except ValueError:
            print("line", number, "skipped:", line)

print("Average:", sum(grades) / len(grades))
line 2 skipped: avi,abc
Average: 85.0

enumerate(file, start=1) donne aussi le numéro de ligne. Le try/except des leçons précédentes permet de sauter une ligne défectueuse sans planter.

Exercice 5: Un fichier qui n’existe pas

  1. Que se passera-t-il si l’on essaie d’ouvrir en lecture un fichier qui n’existe pas ?
  2. Écrivez une fonction load_tasks(path) qui renvoie les lignes du fichier sous forme de liste, et une liste vide si le fichier n’existe pas. Utilisez Path(path).exists().
print(load_tasks("todo.txt"))
with open("todo.txt", "w", encoding="utf-8") as file:
    file.write("buy milk\ncall mom\n")
print(load_tasks("todo.txt"))

Résultat attendu :

[]
['buy milk', 'call mom']
Afficher la solution
  1. Une FileNotFoundError sera lancée.
from pathlib import Path


def load_tasks(path):
    if not Path(path).exists():
        return []
    tasks = []
    with open(path, "r", encoding="utf-8") as file:
        for line in file:
            tasks.append(line.strip())
    return tasks


print(load_tasks("todo.txt"))
with open("todo.txt", "w", encoding="utf-8") as file:
    file.write("buy milk\ncall mom\n")
print(load_tasks("todo.txt"))
[]
['buy milk', 'call mom']

Vérifiez avant l’ouverture si le fichier existe. À la première exécution d’un programme, il n’y a pas encore de fichier — et c’est un état valide, pas une erreur.

Résumé

  • open(path, mode, encoding="utf-8") : "r" lecture (par défaut), "w" écriture qui supprime, "a" ajout à la fin.
  • Toujours with : le fichier est fermé automatiquement, même en cas d’erreur.
  • Lecture : read() (tout), for line in file (ligne par ligne, recommandé). strip() supprime le \n.
  • write n’écrit qu’un str, et sans saut de ligne. Ajoutez "\n" vous-même.
  • Un chemin robuste : Path(__file__).parent / "file.txt". Vérifiez l’existence avec .exists() ou attrapez FileNotFoundError.

Documentation officielle