# Travailler avec des fichiers texte : open, with, lecture et écriture<no value>

## 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

| Mode | Nom | Si le fichier existe | S'il n'existe pas | Actions |
|---|---|---|---|---|
| `"r"` | Read (par défaut) | L'ouvre en lecture | **`FileNotFoundError`** | Lecture seule |
| `"w"` | Write | **Supprime tout le contenu !** | Le crée | Écriture |
| `"a"` | Append | Garde le contenu, et écrit **à la fin** | Le crée | Écriture à la fin |

{{< box "warn" "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"`.
{{< /box >}}

### 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

```python
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`.**

{{< box "analogy" "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.
{{< /box >}}

### Les façons de lire

| Façon | Renvoie | Quand |
|---|---|---|
| `file.read()` | Une seule chaîne avec **tout** le fichier | Petits 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 ligne | **La 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**.

```python
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

```python
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` :

```python
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 :**

```text
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

| Ligne | Explication |
|---|---|
| `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 `with` | Le 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 sortie | Le 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` :

```python
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 :**

```text
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` :

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

Rina,64
```

`week-2/files/grades_report.py` :

```python
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 :**

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

**`report.txt` :**

```text
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.**

```python
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 :

```text
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`.**

```python
file.write(42)
```

```text
TypeError: write() argument must be str, not int
```

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

**Erreur 4 : oublier `\n`.**

```python
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.**

```python
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.

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## 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 :**

```text
Day 1: started
Day 2: networks
```

{{< solution >}}
```python
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="")
```

```text
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`.
{{< /solution >}}

### Exercice 2: w face à a

```python
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.

{{< solution >}}
```python
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="")
```

```text
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`.
{{< /solution >}}

### 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 :

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

**Résultat attendu :**

```text
dana 90
avi 75
noa 88
```

{{< solution >}}
```python
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))
```

```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.
{{< /solution >}}

### 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.

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

**Résultat attendu :**

```text
line 2 skipped: avi,abc
Average: 85.0
```

{{< solution >}}
```python
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))
```

```text
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.
{{< /solution >}}

### 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()`.

```python
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 :**

```text
[]
['buy milk', 'call mom']
```

{{< solution >}}
1. Une **`FileNotFoundError`** sera lancée.
2.

```python
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"))
```

```text
[]
['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.
{{< /solution >}}

<!-- exercises:end -->

## 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`.

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

## Documentation officielle

- [Reading and Writing Files](https://docs.python.org/3/tutorial/inputoutput.html#reading-and-writing-files)
- [Methods of File Objects](https://docs.python.org/3/tutorial/inputoutput.html#methods-of-file-objects)
- [open()](https://docs.python.org/3/builtins/functions.html#open)

<!-- docs:end -->
