Aller au contenu principal

Lecture et écriture de fichiers

Ce que ce chapitre apporte 7 points
  • Comprendre qu'un fichier texte est une suite de caractères, et que le saut de ligne en est un.
  • Ouvrir un fichier avec open() et choisir le bon mode (r, w, a).
  • Suivre le curseur de lecture, et savoir pourquoi un fichier ne se lit qu'une fois.
  • Écrire, ajouter, et découper des lignes en colonnes exploitables.
  • Lire un export de tableur avec le module csv, qui respecte les champs entre guillemets.
  • Se protéger d'un fichier absent avec try / except, et distinguer plusieurs erreurs avec as e, else et finally.
  • Enregistrer et relire une structure de données au format JSON.

Un problème saute peut-être aux yeux : dès que le programme se termine, tout ce qu'il avait en mémoire disparaît. Les notes saisies, un score, une liste, tout est perdu. Pour conserver des données d'une exécution à l'autre, il faut les enregistrer dans un fichier, puis pouvoir les relire.

C'est aussi la porte d'entrée des données réelles. Un relevé de capteurs, un export de logiciel de gestion, une liste de clients : tout cela arrive sous forme de fichier texte, et un programme qui sait le lire sait traiter en une seconde ce qui prendrait une journée à la main.

Astuce : les blocs de cette page fonctionnent aussi pour les fichiers. Le moteur Python du navigateur dispose d'un petit disque virtuel : un même exemple peut écrire puis relire un fichier et montrer le résultat.

Ce qu'est vraiment un fichier texte

Un fichier texte n'est pas un tableau de lignes. C'est une seule longue chaîne de caractères, dans laquelle le passage à la ligne est un caractère comme les autres, noté \n. Un fichier qui semble contenir trois notes contient en réalité neuf caractères :

1 5 \n 1 8 \n 1 2 \n

Cette idée paraît anodine, et pourtant c'est elle qui explique presque tout le chapitre : pourquoi une ligne lue se termine par un caractère invisible, pourquoi deux écritures se collent l'une à l'autre, et pourquoi une position dans le fichier se compte en caractères et non en lignes.

La figure ci-dessous déroule une lecture sur ce fichier de neuf caractères. Chaque case est un caractère, le saut de ligne s'affiche ⏎, et le trait vertical est la position de lecture. Ce qui est estompé a déjà été consommé.

notes.txtfermé1 / 6

sur le disque

15⏎18⏎12⏎
curseur
fichier fermé
renvoie
rien
Une lecture ligne à ligne. Le saut de ligne fait partie de ce que readline renvoie, et le quatrième appel, arrivé au bout, ne renvoie pas d'erreur.

Deux observations valent d'être faites sur cette figure, parce qu'elles reviendront comme causes de bogues dans tout le reste du chapitre : la valeur renvoyée contient le \n final, et l'appel de trop ne provoque aucune erreur.

Ouvrir : le mode décide de tout

On ouvre un fichier avec open("nom", "mode"). Le mode dit ce qu'on veut en faire, et il agit dès l'ouverture, avant toute lecture ou écriture :

ModeNomEffet à l'ouvertureCurseur placé
"r"readlecture seule, erreur si le fichier n'existe pasau début
"w"writecrée le fichier, ou vide celui qui existeau début
"a"appendcrée le fichier, ou conserve celui qui existeà la fin

La ligne du milieu est la plus coûteuse en pratique. Le mode "w" ne remplace pas le contenu au moment où l'on écrit : il le supprime à l'instant de l'ouverture. Un programme qui ouvre en "w" puis plante trois lignes plus loin laisse un fichier vide, et l'ancien contenu n'existe plus nulle part.

notes.txtfermé1 / 2

sur le disque

15⏎18⏎
curseur
fichier fermé
renvoie
rien
Une seule opération, aucune écriture, et le fichier est déjà vide. C'est la perte de données la plus banale du chapitre.

Refermer sans y penser : with

Un fichier ouvert doit être refermé. Ce n'est pas une politesse : tant qu'il est ouvert, ce qui a été écrit peut encore se trouver dans un tampon en mémoire et pas sur le disque. Un programme interrompu avant la fermeture laisse un fichier incomplet, parfois vide, alors que toutes les lignes de write se sont exécutées sans erreur.

La forme with open(...) as f: s'en charge. Le fichier est refermé à la sortie du bloc, y compris si une erreur survient au milieu, ce qu'un close() placé à la dernière ligne ne garantit pas.

Écrire :

main.py
Sortie
>_ Prêt à exécuter…

Relire tout de suite :

main.py
Sortie
>_ Prêt à exécuter…

Le bloc with marque aussi visuellement la zone où le fichier est utilisable. En dehors de l'indentation, f existe encore comme variable, mais toute opération dessus lève une erreur : le fichier est fermé. C'est une limite utile, car elle pousse à sortir du bloc dès que les données sont chargées, plutôt qu'à faire tout le traitement fichier ouvert.

Vérification rapideon peut se reprendre

1.Ouvrir un fichier en mode "w" sur un fichier qui existe déjà fait quoi ?

2.À quoi sert with open(...) as f: plutôt qu'un open suivi d'un close ?

Lire ligne par ligne

Pour traiter un fichier ligne à ligne, on le parcourt comme une séquence. La méthode .strip() retire les espaces et le saut de ligne aux deux extrémités :

main.py
Sortie
>_ Prêt à exécuter…

Sans .strip(), chaque valeur traînerait son \n. Les conséquences sont discrètes et agaçantes : int("12\n") fonctionne, parce que la conversion tolère les espaces, mais "12\n" == "12" est faux, et un affichage se retrouve espacé d'une ligne sur deux. La règle de survie tient en une phrase : une ligne lue se nettoie avant d'être utilisée.

Trois façons de lire coexistent, et elles ne servent pas aux mêmes fichiers :

AppelRenvoieQuand l'employer
f.read()tout le fichier en une chaînefichier court, traité d'un bloc
f.readlines()la liste de toutes les lignesfichier court, dont on veut l'ordre ou le nombre
for ligne in f:les lignes une par unepar défaut, et obligatoire sur un gros fichier

La boucle for a un avantage qui ne se voit pas sur un fichier de trois lignes : elle ne charge qu'une ligne à la fois en mémoire. Sur un relevé de capteur d'un million de lignes, read() occupe autant de mémoire que le fichier entier, tandis que la boucle en occupe une ligne.

Le curseur, ou pourquoi un fichier ne se lit qu'une fois

Un fichier ouvert possède une position de lecture, qui avance à chaque caractère consommé et ne recule jamais toute seule. Elle se compte en caractères depuis le début, saut de ligne compris.

La conséquence surprend toujours : après un premier parcours, il n'y a plus rien à lire. Un second read() ne renvoie pas le fichier, ni une erreur, mais la chaîne vide. Une seconde boucle for ne s'exécute tout simplement pas, et le programme reste muet sans rien signaler.

notes.txtfermé1 / 6

sur le disque

15⏎18⏎
curseur
fichier fermé
renvoie
rien
Lire deux fois de suite, puis revenir au début. Le second appel ne renvoie rien parce qu'il ne reste rien devant le curseur, et seul seek le ramène en arrière.

Ce comportement a une raison : un fichier peut être immense, et rien ne garantit qu'il tienne en mémoire. Python ne le conserve donc pas ; il se souvient seulement de l'endroit où la lecture en est.

Deux réponses existent, et la seconde est presque toujours la bonne :

  • f.seek(0) ramène le curseur au début, ce qui relit le fichier sur le disque.
  • Conserver les lignes dans une liste lors du premier parcours, puis travailler sur la liste autant de fois que nécessaire.
main.py
Sortie
>_ Prêt à exécuter…
Vérification rapideon peut se reprendre

1.Une ligne lue dans un fichier texte se termine par quoi ?

2.Un fichier lu ligne par ligne dans une boucle for, puis relu dans une seconde boucle sans le rouvrir : la seconde boucle voit quoi ?

Écrire : le saut de ligne n'est pas automatique

f.write() écrit exactement ce qu'on lui donne, et rien de plus. À la différence de print(), qui ajoute un passage à la ligne, write ne suppose rien. Deux écritures successives se retrouvent donc collées sur la même ligne.

mesures.txtfermé1 / 4

sur le disque

aucun fichier de ce nom

curseur
fichier fermé
renvoie
rien
Deux appels à write, et une seule ligne dans le fichier : rien n'a séparé les valeurs. Le saut de ligne est à la charge de celui qui écrit.

Le fichier contient bien deux nombres, mais plus aucun moyen de savoir où s'arrête le premier. La relecture donnera une valeur unique, et l'erreur ne se manifestera qu'à ce moment-là, loin de sa cause.

La correction tient au caractère ajouté à chaque écriture :

main.py
Sortie
>_ Prêt à exécuter…

Deux points méritent d'être notés au passage. write attend une chaîne : écrire un nombre directement lève une erreur, d'où le f"{m}" qui le convertit. Et pour une liste déjà prête, "\n".join(...) évite la boucle :

main.py
Sortie
>_ Prêt à exécuter…

Ajouter sans écraser

Le mode "a" ouvre le fichier avec le curseur déjà placé à la fin. C'est ce détail, et non une règle séparée, qui fait que l'ancien contenu survit.

journal.txtfermé1 / 3

sur le disque

08:00␣demarrage⏎
curseur
fichier fermé
renvoie
rien
En mode ajout, le curseur est à la fin dès l'ouverture. La ligne existante n'est jamais traversée, donc jamais recouverte.

C'est le mode du journal : un programme qui tourne en continu ajoute une ligne à chaque événement, sans jamais relire ni réécrire ce qui précède.

main.py
Sortie
>_ Prêt à exécuter…

Un fichier de journal ouvert en "w" par erreur perd tout l'historique à chaque démarrage du programme, et personne ne s'en aperçoit tant que l'historique ne sert pas.

Des lignes aux données : une colonne par valeur

Les fichiers réels contiennent rarement une seule valeur par ligne. Le format le plus répandu met plusieurs champs séparés par un point-virgule ou une virgule, et porte l'extension .csv :

capteur;temperature;humidite
A12;21.4;57
A12;22.1;55
B03;19.8;61

Le traitement suit toujours les mêmes trois gestes : nettoyer la ligne, la découper avec .split(";"), puis convertir les champs qui sont des nombres.

main.py
Sortie
>_ Prêt à exécuter…

L'astuce de ce programme se trouve à la ligne entete = f.readline(). Un seul appel consomme la ligne de titres, et la boucle qui suit reprend là où le curseur s'est arrêté, donc à la première ligne de données. C'est le curseur de la section précédente, employé cette fois à dessein.

Un point de vigilance : .split(";") renvoie toujours des chaînes, même quand le fichier contient des nombres. Sans conversion par float() ou int(), l'addition concatène au lieu d'additionner, et le tri classe "100" avant "9".

Le module csv, pour les fichiers de tableur

.split(";") suffit tant qu'aucun champ ne contient lui-même le séparateur. Un tableur ne s'en prive pas : une cellule « Atelier; poste 3 » est exportée entre guillemets, "Atelier; poste 3", justement pour signaler que ce point-virgule-là fait partie du texte. .split ne connaît pas cette convention et coupe partout. Le module csv de la bibliothèque standard, lui, la respecte.

main.py
Sortie
>_ Prêt à exécuter…

Le découpage à la main rend quatre champs au lieu de trois, et la température glisse en quatrième position : float(champs[2]) tenterait de convertir ' poste 3"'. csv.reader rend chaque ligne sous forme de liste de champs, guillemets retirés, séparateur intérieur conservé. Le paramètre delimiter=";" est indispensable avec un export français, car le module attend par défaut une virgule. Quant à newline="" à l'ouverture, c'est la recommandation de la documentation : il laisse au module le soin des fins de ligne, y compris celles qui se trouvent à l'intérieur d'un champ entre guillemets.

Mieux encore, csv.DictReader lit lui-même la ligne de titres et rend chaque ligne sous forme de dictionnaire : on désigne une colonne par son nom plutôt que par sa position.

main.py
Sortie
>_ Prêt à exécuter…

Plus de readline() pour sauter l'en-tête, plus d'indice à compter : ligne["temperature"] reste juste le jour où quelqu'un insère une colonne dans le tableur. Les valeurs, en revanche, restent des chaînes, comme avec .split : la conversion par float() ne disparaît pas. Pour écrire, csv.writer(f, delimiter=";") et sa méthode writerow([...]) font le chemin inverse, et ajoutent d'eux-mêmes les guillemets là où il en faut.

Vérification rapideon peut se reprendre

1.Une boucle appelle f.write(str(valeur)) sans autre caractère. Le fichier obtenu contient quoi ?

2.Pourquoi préférer csv.reader à ligne.split(";") sur un export de tableur ?

Les accents, et le paramètre qui les sauve

Un fichier texte ne contient pas des lettres mais des octets, et la table qui relie les deux s'appelle l'encodage. Tant que le texte reste en ASCII, tout le monde s'entend. Dès qu'un accent apparaît, l'accord doit être explicite, sans quoi « Café » se relit « Café » ou provoque une UnicodeDecodeError.

La précaution tient en un paramètre, à mettre à l'écriture comme à la lecture :

main.py
Sortie
>_ Prêt à exécuter…

utf-8 est aujourd'hui le standard. Un fichier exporté par un tableur ancien sous Windows peut réclamer encoding="latin-1" ou encoding="cp1252" : c'est la première chose à essayer devant un fichier illisible.

Quand le fichier n'est pas là

Ouvrir en lecture un fichier absent lève une FileNotFoundError, et le programme s'arrête. C'est le cas le plus fréquent en production, parce que le nom vient souvent de l'extérieur : une saisie, un argument, un dossier partagé.

inexistant.txtfermé1 / 2

sur le disque

aucun fichier de ce nom

curseur
fichier fermé
renvoie
rien
Aucun fichier de ce nom : l'ouverture échoue, et surtout ne crée rien. Le mode r ne fabrique jamais le fichier qui lui manque.

Le try / except transforme cet arrêt en message utile :

main.py
Sortie
>_ Prêt à exécuter…

Deux autres erreurs valent d'être connues, car elles disent exactement ce qui cloche :

ErreurCause
FileNotFoundErrorlecture d'un fichier qui n'existe pas
PermissionErrorfichier protégé, ou déjà ouvert par un autre logiciel
UnicodeDecodeErrorencodage déclaré différent de l'encodage réel

Plusieurs erreurs, un seul try

Le try / except a été rencontré au chapitre Conditions et boucles, sur une saisie qui refusait de devenir un nombre. Un traitement de fichier en demande davantage, parce qu'il peut échouer de deux façons très différentes : le fichier manque, ou il est là mais une ligne est illisible. Le bloc suivant traite les deux séparément, et emploie les quatre éléments de la forme complète.

main.py
Sortie
>_ Prêt à exécuter…

Chaque élément a son rôle.

  • Plusieurs except : Python les essaie dans l'ordre, et seul le premier qui correspond à l'erreur s'exécute. Le cas le plus précis se place donc en premier.
  • as e donne un nom à l'erreur interceptée. L'afficher rend le message de Python, qui en dit souvent plus qu'on ne l'aurait écrit : ici, 'n/a\n' montre à la fois la valeur fautive et son saut de ligne. C'est la forme except ValueError as e qu'emploie le chapitre sur NumPy.
  • else ne s'exécute que si aucune erreur n'a eu lieu. Il permet de réduire le try aux lignes qui peuvent échouer : une erreur dans le calcul de la moyenne ne sera pas prise pour une ligne illisible.
  • finally s'exécute toujours, erreur ou non, même si la fonction sort par un return. Le with refermant déjà le fichier, il ne sert ici qu'au message de fin ; ailleurs, il remet un appareil dans un état sûr quoi qu'il arrive.
Un except qui attrape tout ne répare rien
except: seul, ou except Exception:, intercepte toutes les erreurs, y compris une faute de frappe dans un nom de variable. Le programme ne plante plus, mais il ne dit plus rien non plus, et le bogue devient invisible.
On nomme toujours l'erreur qu'on sait traiter, et l'on laisse les autres arrêter le programme.
Vérification rapideon peut se reprendre

Dans un try / except / else / finally, quel bloc s'exécute même quand une erreur a été interceptée ?

Le raccourci, quand le fichier est court

Pour un fichier qui tient en mémoire, le module pathlib réduit les trois lignes à une seule, avec la fermeture comprise :

main.py
Sortie
>_ Prêt à exécuter…

.splitlines() découpe et retire les sauts de ligne au passage, ce qui évite le .strip() de chaque ligne. Sur un gros fichier, en revanche, la boucle for ligne in f: reste la seule option raisonnable, puisque read_text charge tout d'un coup.

Enregistrer une structure : le format JSON

Un fichier texte range bien une liste de nombres, un par ligne. Un dictionnaire qui contient des listes, qui contiennent elles-mêmes des dictionnaires, n'a pas de forme évidente en lignes et en colonnes. Pour ces données-là, le format d'échange standard est JSON : du texte lisible, dont la syntaxe ressemble à s'y méprendre à celle des dictionnaires et des listes de Python. C'est sous cette forme qu'arrivent les réponses de la plupart des services web, bien des fichiers de configuration, ou la commande que reçoit une fonction dans l'atelier sur les fonctions avancées.

Le module json fait la traduction dans les deux sens. json.dumps transforme une structure Python en chaîne JSON, json.loads fait le chemin inverse :

main.py
Sortie
>_ Prêt à exécuter…

La correspondance est presque mot pour mot :

PythonJSON
dictobjet { }
list, tupletableau [ ]
strchaîne, toujours entre guillemets doubles
int, floatnombre
True, Falsetrue, false
Nonenull

Avec un fichier, on retire le s final, qui voulait dire string : json.dump(donnees, f) écrit dans un fichier ouvert, json.load(f) en relit le contenu.

main.py
Sortie
>_ Prêt à exécuter…

indent=2 met une valeur par ligne, avec retrait, pour qu'un humain puisse relire et corriger le fichier ; sans lui, tout tient sur une seule ligne. ensure_ascii=False garde les accents tels quels au lieu de les écrire \u00e9, d'où l'encoding="utf-8" à l'ouverture.

Ce qui ne revient pas à l'identique
JSON ne connaît qu'un seul type de clé : la chaîne. Un dictionnaire dont les clés sont des entiers revient avec des clés "1", "2", et relu[1] lève une KeyError.
De même, un tuple revient sous forme de liste, et un ensemble ne s'écrit pas du tout : json.dumps lève une TypeError.
main.py
Sortie
>_ Prêt à exécuter…
Vérification rapideon peut se reprendre

json.load(f) et json.loads(texte) : quelle différence ?

Mettre en pratique

Exercice : le relevé de température. Trois mesures sont données. Il s'agit de les enregistrer dans releve.txt, une par ligne, puis de relire le fichier pour afficher chaque mesure et la moyenne.

main.py
Sortie
>_ Prêt à exécuter…
Afficher la solution
main.py
Sortie
>_ Prêt à exécuter…

Trois précautions se cumulent ici : le \n ajouté à l'écriture, le .strip() qui le retire à la lecture, et le float() sans lequel sum refuserait d'additionner des chaînes. La liste relues permet ensuite de reparcourir les valeurs autant de fois qu'il le faut, ce que le fichier n'aurait pas permis.

Pour aller plus loin : reprendre le relevé au format date;temperature, puis n'afficher que les journées au-dessus de 20 °C. Le découpage par .split(";") et la conversion du second champ suffisent.

Chapitre révisé en octobre 2026 · Une erreur dans ce chapitre ?

Dans la bibliothèque d'exercices