La gestion de fichiers intégrée de Python est l'un des véritables points forts du langage — aucun import n'est nécessaire pour les opérations de lecture/écriture de base, et l'API est suffisamment claire pour s'apprendre en une après-midi. Mais il y a un vrai fossé entre la version tutoriel et ce que vous déploieriez réellement en production. La version tutoriel ouvre un fichier, le lit et le ferme. La version production gère les incompatibilités d'encodage qui corrompent les données silencieusement, les chemins qui fonctionnent sur macOS mais plantent sur Windows, et les fichiers journaux qui consomment silencieusement toute votre mémoire si vous appelez read() sur un fichier de 2 Go. Cet article couvre les patterns qui tiennent la route — pas seulement le chemin heureux.

L'instruction with — Utilisez-la toujours

Chaque exemple de gestion de fichiers en Python devrait utiliser un gestionnaire de contexte — le bloc with qui garantit que le fichier est fermé même si une exception est levée en cours de lecture. Un gestionnaire de contexte est un objet qui définit ce qui se passe à l'entrée et à la sortie d'un bloc with ; pour les fichiers, la sortie signifie que close() est appelé automatiquement. Voici pourquoi cela compte en pratique :

python
# ❌ Manual close — works until it doesn't
f = open('app.log')
data = f.read()   # if this raises an exception...
f.close()         # ...this line never runs. File handle leaks.

# ✅ Context manager — close() is guaranteed
with open('app.log') as f:
    data = f.read()
# file is closed here, no matter what happened inside the block

Sur des serveurs à longue durée de vie, ce n'est pas théorique — laisser fuir des descripteurs de fichiers provoque finalement OSError: [Errno 24] Too many open files. L'instruction with ne coûte rien et prévient entièrement cette classe de bogues. Utilisez-la partout.

Lire des fichiers — Quatre méthodes, un bon outil pour chaque cas

Python vous fournit plusieurs méthodes sur un objet fichier, et choisir la bonne est plus important que la plupart des tutoriels ne l'admettent :

  • f.read() — lit l'intégralité du fichier dans une seule chaîne. Correct pour les petits fichiers de configuration, dangereux pour les gros.
  • f.readline() — lit une ligne à la fois, avançant le pointeur interne. Utile quand vous avez besoin d'un contrôle manuel de l'itération.
  • f.readlines() — lit toutes les lignes dans une liste. Pratique, mais charge quand même tout le fichier en mémoire.
  • for line in f: — le protocole itérateur. Lit une ligne à la fois sans charger le fichier entier. C'est celui qu'il faut utiliser par défaut.

Voici un exemple réaliste : lire un fichier de configuration de style .env et le transformer en dictionnaire. C'est le genre de chose que vous écrivez vraiment, pas une démo artificielle "lire hello.txt" :

python
from pathlib import Path

def load_config(path: str) -> dict:
    """Read a key=value config file, ignoring comments and blank lines."""
    config = {}
    with open(path, encoding='utf-8') as f:
        for line in f:
            line = line.strip()
            if not line or line.startswith('#'):
                continue
            if '=' not in line:
                continue
            key, _, value = line.partition('=')
            config[key.strip()] = value.strip()
    return config

# Usage
settings = load_config('config/app.conf')
db_host = settings.get('DB_HOST', 'localhost')
L'habitude .strip() : lors de la lecture de lignes, chaque ligne sauf la dernière inclut un \n final (et sur Windows, \r\n). Appelez line.strip() pour supprimer les deux. Si vous voulez seulement supprimer le saut de ligne sans les espaces de début, utilisez line.rstrip('\n') à la place.

Écriture et ajout — Savoir quel mode détruit les données

Le deuxième argument de open() est le mode. Deux modes posent problème régulièrement :

  • 'w' — mode écriture. Ouvre le fichier en écriture. Si le fichier existe déjà, il est tronqué à zéro octet immédiatement — avant que vous écriviez un seul caractère. C'est une destruction silencieuse de données si vous ouvrez le mauvais chemin.
  • 'a' — mode ajout. Ouvre le fichier et déplace le pointeur d'écriture à la fin. Le contenu existant n'est jamais touché. Les nouvelles écritures vont après ce qui était déjà là.

Un bon cas d'usage pour le mode ajout est l'écriture d'un fichier journal structuré avec horodatages. Voici un pattern utile dans les scripts et les petits services :

python
import datetime

LOG_FILE = 'logs/pipeline.log'

def log_event(level: str, message: str) -> None:
    timestamp = datetime.datetime.utcnow().isoformat() + 'Z'
    line = f"[{timestamp}] {level.upper()}: {message}\n"
    with open(LOG_FILE, 'a', encoding='utf-8') as f:
        f.write(line)

log_event('info', 'Pipeline started')
log_event('warning', 'Retrying connection to database')
log_event('error', 'Failed to parse row 4821 — skipping')
Avertissement : open(path, 'w') crée le fichier s'il n'existe pas — ce qui est pratique — mais il détruit silencieusement le fichier s'il existe. Un chemin mal tapé peut effacer un fichier de production sans aucun message d'erreur. Si vous n'êtes pas sûr que le fichier devrait être écrasé, vérifiez d'abord avec Path(path).exists() ou utilisez le mode 'x', qui lève FileExistsError au lieu d'écraser.

Encodage — Le bogue qui touche tout le monde tôt ou tard

C'est la source unique la plus commune de corruption silencieuse de données dans la gestion de fichiers Python. L'encodage par défaut de Python 3 quand vous appelez open() sans en spécifier un est déterminé par locale.getpreferredencoding() — qui sur Windows est typiquement cp1252, et sur Linux/macOS est généralement UTF-8. Cela signifie que du code qui fonctionne parfaitement sur votre Mac peut silencieusement déformer ou planter sur un serveur Windows quand le fichier contient un caractère hors ASCII. La correction est un argument supplémentaire :

python
# ❌ Platform-dependent — works on Linux, corrupts on Windows
with open('customers.csv') as f:
    data = f.read()

# ✅ Explicit UTF-8 — same behavior on every platform
with open('customers.csv', encoding='utf-8') as f:
    data = f.read()

# For files exported from Excel on Windows — may have a BOM (byte order mark)
# utf-8-sig strips the BOM automatically on read
with open('export.csv', encoding='utf-8-sig') as f:
    data = f.read()

Le problème BOM est particulièrement courant avec les fichiers CSV exportés depuis Microsoft Excel — le fichier commence par un caractère caché \ufeff qui apparaît comme  si lu avec le mauvais encodage, ou fait ressembler le premier en-tête de colonne à name au lieu de name. Utiliser encoding='utf-8-sig' gère cela de façon transparente. Voir la documentation Python codecs pour la liste complète des noms d'encodage.

Règle de base : Passez toujours encoding='utf-8' (ou 'utf-8-sig' pour les exports Excel) à chaque appel open(). Faites-en une habitude — cela ne coûte rien et élimine toute une catégorie de bogues spécifiques à l'environnement.

Travailler avec les chemins — Utilisez pathlib

L'ancienne façon de construire des chemins de fichiers en Python était la concaténation de chaînes ou os.path.join(). La façon moderne est pathlib.Path, disponible depuis Python 3.4 et pleinement mature depuis la 3.6. Il gère les séparateurs de chemin correctement sur Windows et Unix sans que vous y pensiez, et remplace une poignée d'appels os.path par un accès aux attributs lisible.

python
from pathlib import Path

# Build a path relative to the current script — works on Windows and Unix
base_dir = Path(__file__).parent
data_dir = base_dir / 'data'
input_file = data_dir / 'records.csv'

# Check existence before opening
if not input_file.exists():
    raise FileNotFoundError(f"Input file not found: {input_file}")

# Create a directory (including parents) without error if it already exists
output_dir = base_dir / 'output' / 'reports'
output_dir.mkdir(parents=True, exist_ok=True)

# Iterate over all JSON files in a directory
for json_file in data_dir.glob('*.json'):
    print(json_file.name)       # just the filename: 'records.json'
    print(json_file.stem)       # filename without extension: 'records'
    print(json_file.suffix)     # extension: '.json'
    print(json_file.parent)     # parent directory as a Path

# The / operator builds paths — no os.path.join needed
report_path = output_dir / f"report_{input_file.stem}.txt"

L'opérateur / ne signifie pas division ici — Path le redéfinit pour signifier la jonction de chemins. Cela se lit naturellement et élimine les problèmes de guillemets et de séparateurs qui viennent avec la construction de chemins basée sur des chaînes. Une autre méthode utile : path.read_text(encoding='utf-8') est un raccourci pour le pattern ouvrir/lire/fermer quand vous voulez juste le contenu du fichier sous forme de chaîne.

Lire des fichiers volumineux sans saturer la mémoire

Quand un fichier est petit — disons, moins de quelques mégaoctets — f.read() ou f.readlines() convient. Quand c'est un journal serveur de 500 Mo ou un export de données de plusieurs gigaoctets, charger tout en mémoire est un chemin rapide vers une MemoryError ou un kill du processus par l'OS. La solution est l'itération ligne par ligne :

python
from pathlib import Path
from collections import Counter

def count_error_levels(log_path: str) -> dict:
    """
    Process a large log file line by line.
    Memory usage stays roughly constant regardless of file size.
    """
    counts = Counter()
    with open(log_path, encoding='utf-8') as f:
        for line in f:
            # Each line is fetched from disk as needed — not loaded all at once
            if ' ERROR ' in line:
                counts['error'] += 1
            elif ' WARN ' in line:
                counts['warning'] += 1
            elif ' INFO ' in line:
                counts['info'] += 1
    return dict(counts)

results = count_error_levels('/var/log/app/server.log')
print(f"Errors: {results.get('error', 0)}, Warnings: {results.get('warning', 0)}")

Le pattern for line in f: fonctionne parce que l'objet fichier Python implémente le protocole itérateur — il récupère les lignes du disque une à la fois en utilisant un tampon interne, donc l'utilisation mémoire est essentiellement constante quelle que soit la taille du fichier. Pour des fichiers vraiment massifs (dizaines de gigaoctets) où même l'itération ligne par ligne n'est pas assez rapide, mmap vous permet de mapper le fichier en mémoire et de le parcourir avec des expressions régulières sans le lire du tout — mais pour la plupart des cas d'usage, l'itérateur de lignes est tout ce qu'il vous faut.

Lire et écrire du JSON et du CSV

Deux formats reviennent constamment dans le travail Python réel, et les deux ont des modules stdlib dédiés qui gèrent les guillemets, l'échappement et la structure correctement — ne les analysez pas avec des découpages de chaînes.

python
import json
import csv
from pathlib import Path

# --- JSON ---
# Reading
with open('config/settings.json', encoding='utf-8') as f:
    settings = json.load(f)            # parsed directly from the file object

# Writing (indent=2 gives readable output)
with open('output/results.json', 'w', encoding='utf-8') as f:
    json.dump(results, f, indent=2, ensure_ascii=False)

# --- CSV ---
# Reading
with open('data/customers.csv', encoding='utf-8-sig', newline='') as f:
    reader = csv.DictReader(f)         # each row is a dict keyed by header
    for row in reader:
        process_customer(row['email'], row['plan'])

# Writing
fieldnames = ['id', 'email', 'plan', 'created_at']
with open('output/export.csv', 'w', encoding='utf-8', newline='') as f:
    writer = csv.DictWriter(f, fieldnames=fieldnames)
    writer.writeheader()
    for record in records:
        writer.writerow(record)

Quelques points à noter : passez newline='' lors de l'ouverture de fichiers CSV — le module csv gère ses propres fins de ligne, et laisser le mode de saut de ligne universel de Python interférer provoque des lignes vides en double sur Windows. Pour JSON, ensure_ascii=False laisse les caractères non-ASCII (lettres accentuées, caractères CJK, etc.) s'écrire tels quels plutôt que d'être échappés en séquences \uXXXX — une sortie bien plus lisible. Si vous travaillez avec des données JSON ou CSV et souhaitez les inspecter ou les transformer visuellement, le Formateur JSON et le Formateur CSV de ce site sont de bons compléments à l'approche par code.

Gestion des erreurs — Les trois exceptions que vous verrez

Les opérations sur les fichiers échouent de façon prévisible. Gérer chaque cas explicitement vous donne des messages d'erreur réellement utiles au lieu d'une trace d'appels générique :

python
import json
from pathlib import Path

def load_json_config(path: str) -> dict:
    """
    Load a JSON config file with explicit error handling.
    Returns the parsed config or raises with a clear message.
    """
    config_path = Path(path)

    try:
        with open(config_path, encoding='utf-8') as f:
            return json.load(f)

    except FileNotFoundError:
        raise FileNotFoundError(
            f"Config file not found: {config_path.resolve()}\n"
            f"Create it or set CONFIG_PATH to the correct location."
        )

    except PermissionError:
        raise PermissionError(
            f"No read permission on {config_path.resolve()}\n"
            f"Check file ownership and mode (chmod 644 on Linux)."
        )

    except UnicodeDecodeError as e:
        raise ValueError(
            f"Encoding error reading {config_path}: {e}\n"
            f"Try opening with encoding='utf-8-sig' if the file came from Windows."
        )

    except json.JSONDecodeError as e:
        raise ValueError(
            f"Invalid JSON in {config_path} at line {e.lineno}, col {e.colno}: {e.msg}"
        )

Les quatre exceptions couvrent presque tous les modes d'échec réels : le fichier n'existe pas, vous n'avez pas la permission, l'encodage est erroné, ou le contenu est malformé. Chaque message dit au prochain développeur (ou à vous à 2h du matin) exactement ce qui s'est mal passé et où chercher. Attraper un Exception brut et afficher "quelque chose s'est mal passé" n'est pas une gestion d'erreur utile — cela déplace juste la confusion en aval.

Quand relever vs retourner None : si un fichier de configuration manquant est une erreur fatale pour votre script, relevez (ou levez une nouvelle exception) pour que l'appelant échoue bruyamment. S'il est optionnel — disons, un fichier de substitution par utilisateur — attraper FileNotFoundError et retourner None ou un dictionnaire par défaut est acceptable. Choisissez un comportement par fonction et documentez-le.

Conclusion

La version courte de tout ce qui précède : utilisez toujours les blocs with, passez toujours encoding='utf-8', utilisez pathlib.Path pour la construction de chemins, et itérez les lignes plutôt que de lire des fichiers entiers quand la taille est inconnue. Ces quatre habitudes éliminent la grande majorité des bogues de gestion de fichiers avant qu'ils n'atteignent la production.

Pour approfondir : la section du tutoriel Python sur la lecture et l'écriture de fichiers couvre les bases de façon approfondie. La documentation pathlib vaut la peine d'être mise en signet — c'est l'une des parties les plus utiles de la stdlib et la plupart des développeurs Python la sous-utilisent. La documentation du module csv et la documentation du module json ont toutes deux de bons exemples pour les cas limites (délimiteurs personnalisés, JSON en streaming, etc.) qui valent la peine d'être lus si vous travaillez régulièrement avec ces formats.