Gérer les chemins de fichiers en Python semble banal, jusqu’au moment où le code doit tourner sur plusieurs systèmes, lire des dossiers, renommer des fichiers ou sécuriser un chemin fourni par un utilisateur. Le besoin derrière path python est presque toujours le même : construire des chemins fiables, les découper proprement et éviter les pièges liés aux séparateurs, aux liens symboliques ou aux chemins relatifs. Dans cet article, je vais aller droit au pratique avec `pathlib`, `os.path`, des exemples concrets et quelques réflexes qui évitent des bugs pénibles en backend comme en script d’automatisation.
L’essentiel à retenir sur les chemins de fichiers en Python
- `pathlib` est l’option la plus lisible pour la majorité des projets modernes.
- `os.path` reste utile quand on travaille au plus bas niveau ou avec du code ancien.
- Assembler des chemins avec des chaînes brutes est une mauvaise habitude, surtout si le code doit rester portable.
- `Path.exists()`, `is_file()`, `is_dir()` et `glob()` couvrent la plupart des vérifications courantes.
- `parent`, `name`, `stem`, `suffix` et `with_suffix()` suffisent souvent pour manipuler des noms de fichiers sans bricolage.
- Un chemin fourni par l’utilisateur doit être traité comme une donnée non fiable, pas comme un simple texte.
Comprendre ce qu’un chemin représente vraiment en Python
Quand on parle de chemins, il faut distinguer trois choses que les débutants mélangent souvent : le chemin relatif, le chemin absolu et la représentation canonique d’un emplacement sur le disque. Un chemin relatif dépend du répertoire de travail courant, donc `logs/app.log` ne pointe pas forcément au même endroit selon la façon dont le script est lancé. Un chemin absolu, lui, commence à partir de la racine du système ou d’un lecteur et reste stable tant que le fichier existe.
En pratique, je garde une règle simple : tant que je peux, je manipule des objets de chemin plutôt que des chaînes. Cela me permet de séparer la logique métier de la syntaxe du système de fichiers. C’est particulièrement utile en backend, où un même code peut tourner sous Linux en production et sous Windows chez un développeur.
from pathlib import Path
relatif = Path("data/exports/report.csv")
absolu = Path("/var/app/data/exports/report.csv")
print(relatif)
print(absolu)
print(Path.cwd())Autre nuance importante : `resolve()` et `absolute()` ne font pas la même chose. Le premier transforme le chemin en version absolue tout en résolvant les liens symboliques et les segments `..` ; le second rend le chemin absolu sans cette normalisation complète. Quand je veux savoir où un fichier pointe réellement, je préfère `resolve()`. Quand je veux seulement afficher ou stocker un chemin absolu sans toucher au système de fichiers, `absolute()` peut suffire. Une fois ce socle posé, le vrai choix devient celui de l’API à utiliser au quotidien.

Choisir entre `pathlib` et `os.path` selon le contexte
Pour la plupart des projets récents, je pars sur `pathlib` sans hésiter. L’API est plus lisible, plus cohérente, et elle réduit le bruit des concaténations manuelles. Cela dit, `os.path` n’est pas obsolète : il reste parfaitement valable si vous maintenez une base de code ancienne, si vous manipulez des chaînes très bas niveau ou si vous avez un besoin précis autour des bytes.
| Besoin | `pathlib` | `os.path` |
|---|---|---|
| Code applicatif lisible | Très adapté, syntaxe orientée objet | Fonctionne, mais plus verbeux |
| Manipulation de chemins | Objet `Path` avec opérateur `/` | Fonctions sur chaînes |
| Compatibilité avec du code ancien | Possible, mais parfois à adapter | Naturelle si le projet l’utilise déjà |
| Lecture rapide du code | Souvent meilleure | Correcte, mais moins expressive |
| Travail très bas niveau sur des chaînes | Moins direct | Pratique dans certains cas |
La différence la plus utile, à mes yeux, est simple : `pathlib` pense en termes d’objets de chemin, alors que `os.path` pense en termes de fonctions. Quand je lis du code, l’objet `Path` raconte immédiatement l’intention : on construit, on inspecte, on parcourt. Quand je maintiens un vieux script ou une bibliothèque qui expose encore des chaînes, je n’ai pas besoin de tout réécrire pour être productif. Une fois l’outil choisi, la priorité devient la construction des chemins eux-mêmes.
Construire des chemins portables sans concaténer des chaînes
Le premier réflexe à oublier est la concaténation de chaînes avec `+` ou des séparateurs codés en dur. C’est fragile, surtout dès qu’on change de plateforme. Avec `pathlib`, je préfère l’opérateur `/`, qui assemble les segments de façon propre et lisible.
from pathlib import Path
base = Path("projet")
journal = base / "logs" / "app.log"
config = Path.home() / ".config" / "mon-app" / "settings.toml"
print(journal)
print(config)Quand un chemin dépend du répertoire courant, je le fais apparaître explicitement au lieu de le laisser implicite. C’est une bonne façon d’éviter les surprises dans les tâches planifiées, les conteneurs ou les commandes lancées depuis un autre dossier.
from pathlib import Path
racine = Path.cwd()
fichier = racine / "data" / "input.json"Pour les chemins saisis par l’utilisateur, `expanduser()` est utile si vous acceptez `~` comme raccourci du répertoire personnel. C’est plus propre que de bricoler soi-même l’expansion. Et si vous devez juste afficher un chemin normalisé ou le transmettre à un autre module, gardez-le en objet `Path` le plus longtemps possible : vous pourrez encore en extraire les morceaux dont vous avez besoin plus tard. Quand le chemin existe déjà sur disque, la question suivante est de savoir quoi vérifier et comment l’explorer.
Vérifier l’existence et parcourir l’arborescence
En pratique, on vérifie surtout trois cas : est-ce que le chemin existe, est-ce que c’est un fichier, est-ce que c’est un dossier ? Avec `Path`, ces tests sont directs et lisibles. Je les utilise souvent avant une lecture, un export ou une suppression, mais sans oublier qu’un test d’existence ne garantit jamais que l’état du fichier restera identique dans la ligne suivante.
from pathlib import Path
chemin = Path("data/import.csv")
if chemin.exists():
if chemin.is_file():
print("Fichier prêt à être lu")
elif chemin.is_dir():
print("Dossier à parcourir")Un point souvent mal compris : `exists()` ne dit pas pourquoi le chemin est absent ou inaccessible. Si vous avez besoin de distinguer un fichier manquant d’un problème de permission, il faut aller plus loin avec `stat()` ou gérer l’exception au bon endroit. En production, c’est généralement plus robuste que de multiplier les tests préalables.
Pour l’exploration d’un dossier, `iterdir()` suffit souvent. Pour filtrer des fichiers, `glob()` et `rglob()` évitent beaucoup de code manuel. J’aime bien cette approche dans les scripts de build, les pipelines d’export et les utilitaires de maintenance.
from pathlib import Path
for fichier in sorted(Path("src").rglob("*.py")):
print(fichier)Le tri explicite est important si l’ordre compte : les résultats de globbing ne sont pas garantis dans un ordre précis. Une fois l’exploration en place, on peut passer à ce qui fait gagner du temps tous les jours : découper et renommer proprement les chemins.
Découper, renommer et reconstruire un chemin proprement
Quand je dois manipuler un nom de fichier, je cherche rarement à recalculer le chemin à la main. `Path` donne déjà les bons attributs : `name`, `stem`, `suffix`, `suffixes`, `parent` et `parents`. Ces propriétés évitent les erreurs de découpage sur les points, les extensions multiples ou les dossiers imbriqués.
from pathlib import Path
fichier = Path("assets/images/logo.svg")
print(fichier.name) # logo.svg
print(fichier.stem) # logo
print(fichier.suffix) # .svg
print(fichier.parent) # assets/images
nouveau = fichier.with_suffix(".png")
print(nouveau) # assets/images/logo.pngLe cas des archives compressées est révélateur : `library.tar.gz` n’a pas une seule extension simple. `suffix` renvoie la dernière partie, alors que `suffixes` liste tous les suffixes. Cette différence compte si vous devez renommer, classifier ou valider des fichiers d’upload.
from pathlib import Path
archive = Path("backup/library.tar.gz")
print(archive.suffix) # .gz
print(archive.suffixes) # ['.tar', '.gz']Quand je veux remonter dans l’arborescence, j’utilise `parent` ou `parents` plutôt que de manipuler des séparateurs. C’est plus lisible, et cela évite les surprises sur Windows avec les lecteurs et les chemins UNC. En revanche, si vous devez remonter dans le système de fichiers réel en tenant compte des liens symboliques, pensez à résoudre d’abord le chemin avec `resolve()`. C’est là que la précision devient plus importante que le confort de lecture.
Sécuriser les chemins fournis par l’utilisateur
Dès qu’un chemin vient de l’extérieur, je le traite comme une donnée potentiellement hostile. Le risque classique, c’est le path traversal : un utilisateur injecte `..` ou un lien symbolique pour sortir du répertoire autorisé. Un simple test sur la chaîne brute ne suffit pas. Il faut raisonner par rapport à une base connue, puis vérifier que le chemin final reste à l’intérieur.
from pathlib import Path
base = Path("/srv/uploads").resolve()
nom_fichier = Path("facture.pdf")
cible = (base / nom_fichier).resolve()
try:
cible.relative_to(base)
except ValueError:
raise ValueError("Chemin refusé")Cette logique est simple, mais elle change tout : on ne se contente pas d’interdire des caractères, on vérifie la position réelle du fichier après normalisation. C’est beaucoup plus solide qu’un filtrage du type “si la chaîne contient `..`, on bloque”. En pratique, je combine aussi cette approche avec une liste blanche de noms attendus ou d’extensions autorisées quand le contexte le demande.
Autre précaution utile : ne laissez pas un utilisateur décider d’un dossier de destination complet si vous pouvez l’éviter. Donnez-lui un nom de fichier ou un identifiant, puis construisez vous-même le chemin final. Cela limite les erreurs et réduit fortement la surface d’attaque. Une fois ce point sécurisé, il reste une confusion fréquente qui mérite d’être levée avant de travailler plus vite.
Ne pas confondre chemins de fichiers et `sys.path`
Le mot “path” en Python crée souvent un malentendu avec `sys.path`. Ici, on ne parle plus du système de fichiers, mais de la liste des répertoires que l’interpréteur consulte pour importer des modules. C’est un sujet voisin, mais ce n’est pas le même problème.
import sys
from pathlib import Path
print(Path.cwd()) # répertoire de travail courant
print(sys.path[0]) # point d’entrée de recherche des importsJ’insiste sur cette différence parce qu’elle explique beaucoup de bugs “mystérieux” en projet Python : un fichier existe bien sur le disque, mais l’import échoue quand même, ou l’inverse. `sys.path` est lié au démarrage de Python, aux environnements virtuels et parfois à `PYTHONPATH`. Ce n’est pas l’outil à utiliser pour gérer un export, une lecture de config ou un chemin d’upload.
Si vous avez besoin de charger un module local, travaillez d’abord sur la structure du projet, l’environnement virtuel et la manière de lancer la commande. Si vous avez besoin d’ouvrir un fichier, revenez à `Path`, `open()` ou `os.path`. Mélanger les deux niveaux finit presque toujours par compliquer le débogage. Avec ces repères, il reste une poignée de réflexes qui font gagner du temps sur la durée.
Les réflexes qui évitent les bugs les plus coûteux
Quand je relis du code qui manipule des chemins, je cherche toujours les mêmes signaux. S’il y a encore des `+` pour assembler des segments, je corrige. Si un chemin utilisateur est injecté directement dans un accès disque, je sécurise. Si un `glob()` alimente un traitement dépendant de l’ordre, je trie explicitement les résultats.
- Utilisez `Path` par défaut pour le code applicatif moderne.
- Réservez `os.path` aux cas hérités ou aux manipulations de bas niveau déjà présentes.
- Gardez les chemins en objets aussi longtemps que possible, puis convertissez seulement à la frontière d’une API qui attend une chaîne.
- Résolvez seulement quand c’est utile, surtout si vous devez suivre des liens symboliques ou verrouiller un chemin.
- Traitez les chemins externes comme non fiables, même s’ils semblent simples au premier regard.
En pratique, la différence entre un script fragile et un code fiable tient souvent à ces détails. Pour la plupart des besoins courants, la combinaison `pathlib` + vérification explicite + base de chemin maîtrisée couvre très loin sans lourdeur. Si vous appliquez ces règles dès maintenant, vous réduisez d’un coup les erreurs de portabilité, les bugs d’import confondus avec des bugs de fichiers et les failles liées aux chemins mal contrôlés.