Aller au contenu principal

Semaine 5 : Lire et écrire des fichiers CSV

📊 Tout ce que tu as appris ce mois-ci menait vers une seule chose : les données réelles vivent dans des fichiers, pas dans du code que tu as tapé à la main.

🎯 Objectifs d'apprentissage

À la fin de cette semaine, tu seras capable de :

  • Ouvrir et fermer un fichier en toute sécurité avec with, et expliquer pourquoi cela compte.
  • Lire les lignes d'un fichier CSV en Python avec le module intégré csv, aussi bien sous forme de listes que de dictionnaires.
  • Écrire des lignes simples et des lignes sous forme de dictionnaires dans un nouveau fichier CSV.
  • Combiner listes, dictionnaires, boucles et fonctions pour résumer un jeu de données réel.
  • Écrire un petit mini-projet de bout en bout, sans aide, en utilisant uniquement ce que tu as appris aux semaines 1 à 4.

Leçon

Ouvrir des fichiers en toute sécurité avec with

Avant d'aborder les CSV spécifiquement, une nouvelle syntaxe : with open(...) as f: ouvre un fichier, te donne accès à un handle vers celui-ci sous le nom f, et le referme automatiquement une fois le bloc indenté terminé — même si une erreur survient en cours de route. Cela compte parce qu'un fichier non fermé peut perdre des écritures mises en mémoire tampon ou verrouiller le fichier pour d'autres programmes. L'alternative, f = open(...) suivi d'un f.close() manuel à la fin, est facile à oublier (surtout si une erreur provoque une sortie prématurée) ; with fait de « toujours le fermer » le comportement par défaut, que tu n'as pas besoin de te rappeler.

CSV : des lignes de valeurs séparées par des virgules

Un fichier CSV (comma-separated values, valeurs séparées par des virgules) est un tableau en texte brut — une ligne par enregistrement, les valeurs séparées par des virgules. Le jeu de données de cette semaine, students-normal.csv, a une ligne d'en-tête (name,quiz1,quiz2,quiz3) suivie d'une ligne par étudiant.

Ce fichier est déjà disponible dans le bac à sable

Le bac à sable du bouton flottant (FAB) a déjà students-normal.csv préchargé — pas besoin de copier-coller quoi que ce soit. open("students-normal.csv") dans votre code ci-dessous le trouvera directement.

import csv

with open("students-normal.csv", newline="") as f:
reader = csv.reader(f)
header = next(reader) # first row: column names
for row in reader:
print(row) # each row is a list of strings

Chaque valeur lue de cette manière est un str — un nombre comme "87" dans le fichier arrive sous forme de chaîne "87", pas de l'entier 87. Tu dois la convertir toi-même, exactement comme input() à la semaine 1. next(reader) extrait uniquement la première ligne (l'en-tête) du lecteur avant que la boucle for ne commence, de sorte que la boucle elle-même ne voit que les lignes de données réelles. L'argument newline="" empêche la gestion propre des fins de ligne du module csv d'entrer en conflit avec celle de Python — c'est du code standard que tu devrais toujours inclure lorsque tu ouvres un fichier pour csv.reader/csv.writer, même si en expliquer précisément la raison dépasse le cadre de cette semaine.

csv.DictReader : des lignes sous forme de dictionnaires

Plutôt que de suivre les positions des colonnes par index, csv.DictReader te donne chaque ligne sous forme de dict indexé par les noms d'en-tête — c'est généralement l'option la plus lisible, et elle lit automatiquement la ligne d'en-tête pour toi (pas besoin de next(reader) manuel) :

with open("students-normal.csv", newline="") as f:
reader = csv.DictReader(f)
for row in reader:
name = row["name"]
score = int(row["quiz1"]) # still a str until you convert it
print(name, score)

Compare ceci à csv.reader : avec csv.reader, row[1] signifie « ce qui se trouve dans la colonne 1 » — fragile si l'ordre des colonnes change un jour. Avec csv.DictReader, row["quiz1"] dit exactement ce que tu veux dire, quel que soit l'ordre des colonnes, au petit coût d'une recherche dans un dictionnaire plutôt que d'un index de liste.

Écrire des fichiers CSV

csv.writer et csv.DictWriter sont l'image miroir de csv.reader/csv.DictReader — utiles pour enregistrer dans un fichier un résumé que tu as calculé :

with open("summary.csv", "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["name", "average"])
writer.writerow(["Amina", 91.5])

Le "w" (mode écriture) est important — open("summary.csv", "w") crée le fichier s'il n'existe pas, et écrase complètement son contenu s'il existe déjà. csv.DictWriter reflète DictReader : tu écris des dictionnaires au lieu de simples listes, et il a besoin de connaître les noms de colonnes (fieldnames) à l'avance pour savoir dans quel ordre les écrire et pouvoir produire une ligne d'en-tête correspondante :

with open("summary.csv", "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=["name", "average"])
writer.writeheader() # writes the header row
writer.writerow({"name": "Amina", "average": 91.5})
writer.writerow({"name": "Youssef", "average": 74.3})

DictWriter est le pendant naturel si tu as construit ton résumé sous forme de liste de dictionnaires (la forme « imbrication de collections » de la semaine 3) plutôt que sous forme d'un simple dictionnaire nom → moyenne.

Mettre le tout ensemble : un mini-projet

Le projet de cette semaine combine tout ce qui vient des semaines 1 à 4 : lire un CSV de notes d'étudiants, calculer la moyenne de chacun avec une fonction, la stocker dans un dict, et afficher un résumé trié du plus haut au plus bas — en utilisant sorted() avec une fonction key, qui elle-même prend une fonction en argument, exactement comme compose dans la question socratique de la semaine dernière :

def average(scores):
return sum(scores) / len(scores)

averages = {} # name -> average
with open("students-normal.csv", newline="") as f:
reader = csv.DictReader(f)
for row in reader:
name = row["name"]
scores = [int(row["quiz1"]), int(row["quiz2"]), int(row["quiz3"])]
averages[name] = average(scores)

for name in sorted(averages, key=lambda n: averages[n], reverse=True):
print(name, round(averages[name], 1))

lambda n: averages[n] est une petite fonction sans nom — lis-la comme « la règle qui associe un nom à sa moyenne », utilisée uniquement pour indiquer à sorted() selon quoi trier. Une lambda est exactement équivalente à un petit def (lambda n: averages[n] se comporte comme la fonction d'une ligne def key_fn(n): return averages[n]) ; elle existe uniquement pour que tu n'aies pas besoin de nommer et de définir une fonction séparée juste pour donner à sorted() une règle simple et jetable.

⚠️ Pièges courants

  • Oublier newline="". Sans cela, csv.writer peut insérer des lignes vides supplémentaires entre les enregistrements sur certains systèmes (notamment Windows) — inclus-le toujours lors de la lecture ou de l'écriture de fichiers CSV.
  • Ouvrir en mode "w" par accident alors que tu voulais ajouter des données. open("summary.csv", "w") efface d'abord le contenu existant du fichier. Si tu veux ajouter des lignes à un fichier existant à la place, utilise le mode "a" (append, ajout).
  • Supposer un ordre de colonnes fixe avec csv.reader. row[0] n'est « le nom » que si la colonne nom est réellement en premier et le reste — un DictReader évite toute cette catégorie de bug.
  • Ne pas convertir les colonnes numériques avant de faire des calculs dessus. row["quiz1"] + row["quiz2"] avec DictReader concatène deux chaînes ("88" + "92""8892"), ça n'additionne pas deux nombres — un écho direct du piège de input() à la semaine 1.

🧩 Défis

Étant donné un CSV avec les colonnes name,score, écris un programme qui calcule et affiche la moyenne des notes sur tous les étudiants du fichier.

Étends le programme précédent pour qu'il affiche aussi le nom de l'étudiant qui a la note la plus élevée.

Écris les moyennes par étudiant que tu as calculées ci-dessus dans un nouveau fichier summary.csv avec les colonnes name,average.

Que se passerait-il si une ligne du CSV avait une note manquante (une chaîne vide au lieu d'un nombre) ? Modifie ton programme pour qu'il ne plante pas sur cette ligne.

Refais le défi 3 en utilisant csv.DictWriter au lieu de csv.writer. Quelle version trouves-tu plus facile à lire, et pourquoi ?

Si tu voulais ajouter la ligne d'un étudiant supplémentaire à summary.csv sans effacer ce qui s'y trouve déjà, quel mode de fichier utiliserais-tu à la place de "w" ? Essaie-le.

🤔 Questions socratiques

  • Pourquoi tout ce qui est lu depuis un CSV arrive-t-il comme un str, même les colonnes qui semblent numériques ? Où ailleurs dans ce cours as-tu déjà vu ce même schéma « texte en entrée, conversion nécessaire » ?
  • sorted(averages, key=lambda n: averages[n], reverse=True) — que changerait le retrait de reverse=True ? Que changerait le retrait de key=... entièrement en écrivant simplement sorted(averages) ?
  • Tu as maintenant construit un petit pipeline : lire → transformer → résumer → écrire. De quels concepts des semaines 1 à 4 chaque étape dépendait-elle réellement ? Sans le contenu de quelle semaine ce projet aurait-il été impossible ?
  • with open(...) as f: ferme automatiquement le fichier même si une erreur survient dans le bloc. Peux-tu imaginer une raison pour laquelle un programme qui ouvre de nombreux fichiers au cours de sa vie, sans jamais les fermer correctement, pourrait finir par échouer de manière étrange ?
  • csv.DictWriter a besoin de fieldnames spécifié à l'avance, avant d'écrire quoi que ce soit. Pourquoi penses-tu qu'il l'exige, plutôt que de simplement déduire les colonnes à partir du premier dictionnaire que tu lui passes via writerow ?

✅ Quiz de la semaine

✅ Quiz de la semaine

1. Quel est le type de chaque valeur lue depuis un fichier CSV avec le module csv, avant que tu ne la convertisses ?
2. Que te donne csv.DictReader pour chaque ligne, comparé à csv.reader ?
3. Dans sorted(names, key=lambda n: averages[n]), à quoi sert l’argument key ?
4. Quel module de la bibliothèque standard de Python a été utilisé cette semaine pour lire/écrire des fichiers CSV ?
5. Ouvrir un fichier avec open("data.csv", "w") alors que le fichier existe déjà va :

🎁 Bonus : un premier aperçu des classes

Disponible une fois le quiz de la semaine réussi.