pathlib : manipuler des chemins de fichiers en Python

pathlib représente un chemin de fichier par un objet plutôt qu'une chaîne, et regroupe ce que os.path éparpillait dans plusieurs modules.
5 min de lecture
Believemy logo

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.

PYTHON
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())  # False

Rien à 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.

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

PYTHON
chemin = Path(__file__).parent / "data" / "rapport.txt"
nom = chemin.stem

Le 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 faireos.path et compagniepathlib
Assembler deux morceauxos.path.join(a, b)a / b
Remonter au dossier parentos.path.dirname(c)c.parent
Le nom sans extensionos.path.splitext(...)[0]c.stem
Lire un fichier entieropen(c).read()c.read_text()
Créer l'arborescenceos.makedirs(c)c.mkdir(parents=True)
Lister les fichiers csvglob.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.

PYTHON
Path("/var/app") / "logs/app.log"   # /var/app/logs/app.log
Path("/var/app") / "/logs/app.log"  # /logs/app.log
Attention

Une 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

Question

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.

Question

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.

Question

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.

Termes connexes

Découvrez notre glossaire Python

Parcourez les termes et définitions les plus couramment utilisés dans le domaine du développement avec Python.

Partager cet article

Tu veux nous aider ? Fais un lien vers cet article sur tes réseaux ou encore mieux : sur ton site, dans un article ou dans ta newsletter.