pathlib à la place de os.path: gérer les chemins modernes
1. Aperçu général
De nombreux anciens codes Python gèrent les chemins avec os.path.join, os.path.splitext, os.listdir… Ces fonctions prennent et retournent des chaînes, rendant le code long, difficile à lire et sujet à erreurs. Le module pathlib (disponible depuis Python 3.4) représente les chemins par un objet Path disposant de toutes les méthodes pour concaténer, scinder, lire, écrire et parcourir les fichiers.
Après cet article, vous pourrez convertir la majeure partie de votre code os.path en pathlib et connaître les pièges courants. Les exemples utilisent Python 3.12.
2. Créer des objets Path
Path est le point de départ pour toute opération :
from pathlib import Path
reports = Path("data/reports")
home = Path.home()
cwd = Path.cwd()
print(reports)
print(type(reports))
print(home)
data/reports
<class 'pathlib.PosixPath'>
/home/minh
Explications :
Path("data/reports")crée un objet chemin. Aucune opération sur le disque n’est effectuée. Le répertoire n’a pas besoin d’exister.- Sur Linux/macOS,
Pathretourne unPosixPath. Sur Windows, il retourne unWindowsPath. Vous n’avez pas besoin de vous soucier de la classe spécifique. Path.home()etPath.cwd()remplacentos.path.expanduser("~")etos.getcwd().
Si vous avez seulement besoin de manipuler des chaînes de chemin sans toucher aux fichiers réels, utilisez PurePath, PurePosixPath ou PureWindowsPath. C’est pratique pour traiter les chemins Windows lors de l’exécution sur Linux.
3. Concaténer les chemins avec l’opérateur /
C’est le changement le plus visible. Au lieu d’imbriquer os.path.join, vous utilisez l’opérateur / :
import os
from pathlib import Path
# Ancienne méthode
old = os.path.join("data", "orders", "2026", "order_1001.json")
# Nouvelle méthode
base = Path("data")
new = base / "orders" / "2026" / "order_1001.json"
print(old)
print(new)
print(new == Path(old))
data/orders/2026/order_1001.json
data/orders/2026/order_1001.json
True
L’opérateur / accepte à la fois Path et str à droite. Le résultat est toujours un Path, vous pouvez donc continuer à concaténer. Le séparateur change automatiquement selon le système d’exploitation : / sur Linux, \ sur Windows.
Si le nombre de composants change à l’exécution, utilisez joinpath :
parts = ["orders", "2026", "order_1001.json"]
path = Path("data").joinpath(*parts)
print(path) # data/orders/2026/order_1001.json
4. Scinder les composants d’un chemin
os.path nécessite plusieurs fonctions pour cela : basename, dirname, splitext. Avec pathlib, tout est un attribut :
from pathlib import Path
invoice = Path("/srv/shop/invoices/2026/invoice_042.final.pdf")
print(invoice.name) # nom complet du fichier
print(invoice.stem) # nom sans la dernière extension
print(invoice.suffix) # dernière extension
print(invoice.suffixes) # toutes les extensions
print(invoice.parent) # répertoire contenant
print(invoice.parents[1])
print(invoice.parts)
invoice_042.final.pdf
invoice_042.final
.pdf
['.final', '.pdf']
/srv/shop/invoices/2026
/srv/shop/invoices
('/', 'srv', 'shop', 'invoices', '2026', 'invoice_042.final.pdf')
Explications :
stemetsuffixne scindent qu’au dernier point. Pour les fichiers.tar.gz, utilisezsuffixes.parentéquivaut àos.path.dirname.parentsest une séquence de répertoires parents, remontant graduellement vers la racine.partsdivise le chemin en tuple, utile pour comparer niveau par niveau.
Pour créer un nouveau chemin avec un nom ou une extension différents, utilisez les méthodes with_* :
invoice = Path("invoices/invoice_042.pdf")
print(invoice.with_suffix(".xml")) # invoices/invoice_042.xml
print(invoice.with_stem("invoice_043")) # invoices/invoice_043.pdf
print(invoice.with_name("summary.csv")) # invoices/summary.csv
Ces méthodes ne changent pas le nom du fichier sur le disque. Elles retournent simplement un nouveau Path, car Path est immuable.
5. Lire et écrire des fichiers
Pour les petits fichiers, vous n’avez pas besoin de open() :
from pathlib import Path
config = Path("app_config.txt")
config.write_text("currency=VND\ntimeout=30\n", encoding="utf-8")
content = config.read_text(encoding="utf-8")
print(content.splitlines())
logo = Path("logo.bin")
logo.write_bytes(b"\x89PNG")
print(logo.read_bytes())
['currency=VND', 'timeout=30']
b'\x89PNG'
write_textouvre le fichier, écrit puis ferme. L’ancien fichier est écrasé.read_textlit tout le contenu en chaîne.write_bytesetread_bytesfonctionnent de la même manière avec des données binaires.
Pour les grands fichiers ou quand vous devez lire ligne par ligne, utilisez toujours un gestionnaire de contexte. Path a une méthode open intégrée :
with Path("orders.csv").open("w", encoding="utf-8") as f:
f.write("id,amount\n1001,250000\n1002,99000\n")
with Path("orders.csv").open(encoding="utf-8") as f:
total = sum(int(line.split(",")[1]) for line in f if not line.startswith("id"))
print(total) # 349000
Attention : Toujours passer
encoding="utf-8". Si vous l’omettez, Python utilise l’encodage par défaut du système. Sur Windows, le défaut est souventcp1252, donc les données en vietnamien seront corrompues.
6. Vérifier et parcourir les répertoires
6.1. Vérifier l’état
from pathlib import Path
p = Path("orders.csv")
print(p.exists(), p.is_file(), p.is_dir())
print(p.stat().st_size)
print(p.resolve())
True True False
36
/home/minh/project/orders.csv
resolve() retourne le chemin absolu et résout les symlinks. Il remplace os.path.abspath et os.path.realpath.
6.2. Lister et trouver des fichiers
from pathlib import Path
root = Path("shop")
(root / "orders" / "2026").mkdir(parents=True, exist_ok=True)
(root / "orders" / "2026" / "o1.json").write_text("{}", encoding="utf-8")
(root / "orders" / "o0.json").write_text("{}", encoding="utf-8")
(root / "readme.md").write_text("# Shop", encoding="utf-8")
print(sorted(p.name for p in root.iterdir()))
print(sorted(str(p) for p in root.glob("*.md")))
print(sorted(str(p) for p in root.rglob("*.json")))
['orders', 'readme.md']
['shop/readme.md']
['shop/orders/2026/o1.json', 'shop/orders/o0.json']
iterdir()remplaceos.listdir(). Il retourne les cheminsPathcomplets, pas seulement les noms, vous n’avez donc pas besoin de les réassembler.glob("*.md")cherche uniquement dans le répertoire actuel.rglob("*.json")cherche de manière récursive, équivalent àglob("**/*.json").
Depuis Python 3.12, Path.walk() remplace os.walk() :
for dirpath, dirnames, filenames in Path("shop").walk():
print(dirpath, filenames)
7. Créer, renommer et supprimer
import shutil
from pathlib import Path
backup = Path("backup/daily")
backup.mkdir(parents=True, exist_ok=True)
src = Path("orders.csv")
dst = src.rename(backup / "orders_2026-10-17.csv")
print(dst)
dst.unlink()
Path("not_there.txt").unlink(missing_ok=True)
backup.rmdir()
shutil.rmtree("backup")
backup/daily/orders_2026-10-17.csv
mkdir(parents=True, exist_ok=True)équivaut àos.makedirs(..., exist_ok=True).renameretourne le nouveauPath. Sur Windows,renamegénère une erreur si le fichier de destination existe. Pour écraser sur toutes les plateformes, utilisezreplace.unlinksupprime un fichier. Avecmissing_ok=True, il n’y a pas d’erreur si le fichier n’existe pas.rmdirne supprime que les répertoires vides. Pour supprimer une arborescence entière, vous avez toujours besoin deshutil.rmtree. Cette fonction accepte lesPathnormalement.
8. Tableau de comparaison rapide
| os / os.path | pathlib |
|---|---|
os.path.join(a, b) |
Path(a) / b |
os.path.basename(p) |
p.name |
os.path.dirname(p) |
p.parent |
os.path.splitext(p) |
p.stem, p.suffix |
os.path.exists(p) |
p.exists() |
os.path.isfile(p) / isdir |
p.is_file() / p.is_dir() |
os.path.abspath(p) |
p.resolve() ou p.absolute() |
os.path.expanduser("~") |
Path.home() ou p.expanduser() |
os.getcwd() |
Path.cwd() |
os.listdir(p) |
p.iterdir() |
glob.glob("**/*.py", recursive=True) |
p.rglob("*.py") |
os.makedirs(p, exist_ok=True) |
p.mkdir(parents=True, exist_ok=True) |
os.remove(p) |
p.unlink() |
os.rename(a, b) |
a.rename(b) |
os.path.getsize(p) |
p.stat().st_size |
os.path.relpath(p, start) |
p.relative_to(start) |
9. Écrire des tests avec pytest
La fixture tmp_path de pytest retourne déjà un Path, donc tester du code utilisant pathlib est très concis. Supposons que vous ayez une fonction qui archive les fichiers PDF de factures dans un répertoire :
# archive.py
from pathlib import Path
def archive_invoices(source: Path, target: Path) -> list[Path]:
target.mkdir(parents=True, exist_ok=True)
moved = []
for pdf in sorted(source.rglob("*.pdf")):
moved.append(pdf.rename(target / pdf.name))
return moved
Le test correspondant :
# test_archive.py
from pathlib import Path
from archive import archive_invoices
def test_archive_moves_only_pdf_files(tmp_path: Path):
source = tmp_path / "inbox"
(source / "2026").mkdir(parents=True)
(source / "a.pdf").write_bytes(b"%PDF")
(source / "2026" / "b.pdf").write_bytes(b"%PDF")
(source / "note.txt").write_text("skip me", encoding="utf-8")
target = tmp_path / "archive"
moved = archive_invoices(source, target)
assert [p.name for p in moved] == ["b.pdf", "a.pdf"]
assert sorted(p.name for p in target.iterdir()) == ["a.pdf", "b.pdf"]
assert (source / "note.txt").exists()
assert not (source / "a.pdf").exists()
pytest -q test_archive.py
. [100%]
1 passed in 0.02s
L’ordre dans moved est b.pdf avant a.pdf. Raison : sorted compare les objets Path, et inbox/2026/b.pdf précède inbox/a.pdf car le caractère 2 est inférieur à a dans l’ordre de tri. Ce détail est facile à oublier quand vous passez de la comparaison de chaînes de noms de fichiers à la comparaison de Path.
10. Erreurs courantes
Additionner Path avec une chaîne. Path("data") + "/x.txt" génère TypeError. Utilisez / à la place de +. Si vous avez vraiment besoin de concaténer au nom du fichier, utilisez p.with_name(p.name + "_old").
Le côté droit est un chemin absolu. Le résultat peut vous surprendre :
from pathlib import Path
print(Path("/srv/uploads") / "/etc/passwd") # /etc/passwd
Un composant absolu à droite remplace toute la partie gauche. Si le nom de fichier vient de l’utilisateur, vous devez vérifier avant utilisation :
base = Path("/srv/uploads").resolve()
candidate = (base / user_input).resolve()
if not candidate.is_relative_to(base):
raise ValueError("Chemin invalide")
with_suffix manque le point. p.with_suffix("pdf") génère ValueError. Vous devez écrire ".pdf".
Oublier que glob retourne un générateur. Vous ne pouvez le parcourir qu’une fois. Si vous devez le réutiliser ou appeler len(), enveloppez-le dans list(...). L’ordre des résultats n’est pas garanti non plus, utilisez donc sorted quand l’ordre importe.
Fichiers cachés et stem. Avec Path(".env"), suffix est une chaîne vide et stem est ".env". N’assumez pas que tous les fichiers ont une extension.
Les vieilles bibliothèques n’acceptent que str. La plupart des API standard acceptent os.PathLike depuis Python 3.6. Si vous rencontrez une vieille bibliothèque, utilisez str(p) ou os.fspath(p).
Attention :
pathlibne remplace pas complètementosetshutil. Pour copier des fichiers, vous utilisez toujoursshutil.copy2(Python 3.14 ajoute seulementPath.copy). Les variables d’environnement sont toujours lues viaos.environ. Les permissions complexes nécessitent toujoursos.chmodouos.access.
Quand ne pas changer ? Pour des boucles traitant des millions de chaînes de chemin sans toucher au disque, créer des objets Path est plus lent que manipuler des chaînes pures. C’est un cas rare, mesurez avant d’optimiser. Pour le code ancien qui fonctionne bien, vous n’avez pas besoin de tout refaire. Convertissez graduellement en touchant chaque module.
11. Conclusion
pathlib rassemble les opérations éparpillées dans os, os.path et glob en un seul objet. Le code est plus court, lit naturellement et a moins d’erreurs de concaténation. Vous avez vu comment concaténer les chemins avec /, scinder les composants avec name, stem, suffix, parent, lire et écrire avec read_text et write_text, parcourir les répertoires avec iterdir, glob, rglob, walk, et tester avec tmp_path de pytest.
Vous devez retenir trois points. Toujours passer encoding. Soyez prudent quand vous concaténez avec des chemins absolus provenant d’entrée externe. Continuez à utiliser shutil pour les opérations comme copier et supprimer des arborescences. Pour le code nouveau, utilisez Path par défaut et ne revenez à os.path que s’il y a une raison spécifique.