Définition
Écrire un chemin à la main pose vite un problème : il faut deviner où finit le dossier, où commence le nom, et quel séparateur utiliser selon le système. pathlib répond à cet ennui en représentant un chemin par un objet plutôt qu'une chaîne. C'est un module de la bibliothèque standard, dont la classe centrale s'appelle Path.
Cette classe rassemble ce qui était réparti entre plusieurs modules : os.path pour le texte du chemin, os pour le système de fichiers, glob pour la recherche, shutil pour copier ou déplacer. Un seul objet remplace quatre imports.
Voici ce que cela donne à l'usage.
from pathlib import Path
dossier = Path("donnees") / "exports"
fichier = dossier / "ventes.csv"
print(fichier) # donnees/exports/ventes.csv
print(fichier.suffix) # .csv
print(fichier.stem) # ventes
print(fichier.exists()) # FalseRien à installer : une seule ligne d'import suffit, le module accompagne Python depuis la version 3.4. Né de la PEP 428, il a été pensé d'un bloc plutôt qu'assemblé au fil des versions.
Le problème qu'il résout
Une chaîne qui contient un chemin reste une chaîne, rien de plus. Elle ne sait pas où finit le dossier, ni où commence l'extension, ni si le fichier existe. Il faut recalculer cela à chaque fois, avec des fonctions dispersées dans plusieurs modules.
Voici ce que cela donne avec l'ancienne approche.
import os.path
chemin = os.path.join(os.path.dirname(__file__), "data", "rapport.txt")
nom = os.path.splitext(os.path.basename(chemin))[0]La même opération avec un objet Path tient sur deux lignes qui se lisent presque à voix haute, sans imbrication à décortiquer.
chemin = Path(__file__).parent / "data" / "rapport.txt"
nom = chemin.stemLe gain dépasse la simple économie de caractères. Assembler des chemins à coups de + produit des barres en double ou des noms collés, et un chemin en dur casse silencieusement sur Windows, où le séparateur attendu est la barre inversée. C'est ce que règle l'opérateur / de pathlib : Path détourne la division par une méthode magique, l'exemple le plus parlant de ce procédé dans la bibliothèque standard.
Face à os.path
Remplacer os.path partout ne rendrait pourtant pas justice à l'ancien module. Il n'est pas déprécié, fonctionne correctement sur des chaînes, et reste même un peu plus rapide sur des millions de répétitions. Ce qui lui manque, c'est la cohérence.
Le tableau suivant met les deux écritures côte à côte, pour les opérations les plus courantes.
| Ce que vous voulez faire | os.path et compagnie | pathlib |
|---|---|---|
| Assembler deux morceaux | os.path.join(a, b) | a / b |
| Remonter au dossier parent | os.path.dirname(c) | c.parent |
| Le nom sans extension | os.path.splitext(...)[0] | c.stem |
| Lire un fichier entier | open(c).read() | c.read_text() |
| Créer l'arborescence | os.makedirs(c) | c.mkdir(parents=True) |
| Lister les fichiers csv | glob.glob(motif) | c.glob("*.csv") |
La quatrième ligne mérite un mot à part. read_text ouvre, lit et referme le fichier en un geste, ce qui dispense du bloc with pour une petite configuration. Sur un fichier volumineux parcouru ligne par ligne, la méthode open de l'objet redevient nécessaire, et le gestionnaire de contexte aussi, faute de quoi le fichier reste ouvert trop longtemps.
Le piège de ceux qui le découvrent
Le premier piège tient à la nature de l'objet. Un Path n'est pas une chaîne, même s'il s'affiche comme telle à l'écran. La quasi-totalité de la bibliothèque standard l'accepte désormais, tout comme pandas. Mais une bibliothèque plus ancienne ou une sérialisation vers du json font lever une TypeError. La conversion par str(chemin) règle le cas.
Le deuxième piège est silencieux, donc plus coûteux : si le membre de droite d'un assemblage est un chemin absolu, il efface tout ce qui le précède, sans avertissement.
Path("/var/app") / "logs/app.log" # /var/app/logs/app.log
Path("/var/app") / "/logs/app.log" # /logs/app.logUne barre oblique de trop dans une valeur de configuration suffit à déclencher ce piège. Le programme écrit alors à la racine du disque, sans avertissement, et l'erreur se découvre souvent bien plus tard.
Le troisième piège vient du dossier de référence : un chemin relatif se résout depuis le répertoire courant du processus, jamais depuis l'emplacement du code. Le script fonctionne lancé depuis son dossier, mais échoue depuis un cron. Path(__file__).resolve().parent ancre le chemin sur le fichier et met fin à ces bugs.
Quand il ne sert à rien
pathlib parle du système de fichiers local, et de lui seul. Une adresse web n'est pas un chemin, même si elle y ressemble : la confier à Path écrase la double barre de https, là où requests fait le travail correctement. Le même problème touche une clé de stockage objet ou une entrée d'archive zip : la ressemblance visuelle trompe, la logique n'est pas la même.
Il y a aussi une question de volume. Sur plusieurs centaines de milliers d'entrées, chaque objet Path créé coûte un peu de mémoire, et os.scandir reste plus économe. Pour un chemin unique, jamais assemblé, une simple chaîne suffit très bien. L'erreur la plus fréquente consiste à convertir par principe, même là où l'objet n'apporte rien.
Questions fréquentes
Faut-il réécrire tout le code qui utilise os.path ?
Non, une réécriture massive n'apporterait qu'un risque de régression. Les deux approches cohabitent sans difficulté, puisque les fonctions de os.path acceptent un objet Path en entrée. Le bon moment pour basculer est quand vous touchez déjà ce fichier pour une autre raison.
Comment parcourir un dossier et tous ses sous-dossiers ?
Avec rglob, la version récursive de glob, dans une boucle ordinaire. Un détail compte : la méthode rend un générateur et non une liste, donc le tester directement pour savoir s'il existe des résultats répond toujours oui, même vide.
Pourquoi tester l'existence avant d'ouvrir un fichier est-il déconseillé ?
Parce que le fichier peut disparaître entre le test et l'ouverture, et la vérification ne dit rien des droits de lecture. Ouvrir directement puis rattraper l'exception donne la vraie réponse, celle du système, plutôt qu'une réponse valable une milliseconde plus tôt.