Leçon 05-2 — Chemins avec pathlib
1. Objectif
À la fin de cette leçon tu sauras : construire des chemins avec pathlib.Path et l'opérateur /, expliquer la différence entre chemin relatif et chemin absolu, diagnostiquer en une minute le grand classique « le fichier existe mais Python dit FileNotFoundError », créer des dossiers (mkdir), tester l'existence (exists), lister des fichiers (glob), et éviter les deux pièges Windows : les \ et l'encodage.
2. Pourquoi c'est important
La leçon 05-1 t'a appris à ouvrir des fichiers qui sont juste à côté. Dès que ton programme touche un fichier ailleurs — un dossier de sauvegardes, le dossier Downloads, un sous-dossier de données — il te faut des chemins. Or les chemins sont la première source de bugs « incompréhensibles » du niveau : « ça marche dans VS Code mais pas dans le terminal », « ça marche chez moi mais pas sur l'autre PC ». Ces bugs ont tous la même explication (le dossier courant), et pathlib est l'outil moderne qui rend les chemins lisibles et portables — Windows, Mac et Linux compris. C'est aussi le cœur du mini-projet de rangement de fichiers, ton premier script qui agit sur ta vraie machine.
3. Explication simple
Un chemin, c'est l'adresse d'un fichier sur le disque. Il y a deux façons de donner une adresse :
- Chemin absolu : l'adresse complète depuis la racine du disque —
C:\Users\jtron\Claude\learnpython\notes.txt. Sans ambiguïté, marche de partout, mais long et propre à TA machine. - Chemin relatif : l'adresse depuis l'endroit où on se trouve —
notes.txtoudata/notes.txt. Court et portable… mais qui dépend de « où on se trouve ».
Et « où on se trouve », pour un programme, c'est le dossier courant : le dossier depuis lequel tu as lancé python dans le terminal (celui affiché par pwd). PAS le dossier où est rangé le fichier .py. C'est LA phrase la plus importante de la leçon.
pathlib fournit un objet Path qui représente un chemin et sait tout faire avec :
from pathlib import Path
p = Path("data") / "notes.txt" # le / assemble les morceaux du chemin
print(p) # data\notes.txt (Python met le bon séparateur)
print(p.exists()) # True ou False
Le / entre deux morceaux de chemin, c'est la signature de pathlib : lisible, et Python choisit tout seul \ sous Windows, / ailleurs.
4. Explication approfondie
a) Le dossier courant, racine de tous les mystères. Quand Python voit open("notes.txt"), il cherche notes.txt dans le dossier courant du processus — celui où le terminal était quand tu as tapé la commande. Conséquence : le même script marche si tu le lances d'un endroit, plante si tu le lances d'un autre. Vérifie toujours avec :
from pathlib import Path
print(Path.cwd()) # cwd = current working directory, le dossier courant
Deux stratégies propres : (1) discipline de lancement — on lance toujours ses programmes depuis la racine du projet, et on écrit les chemins relatifs depuis cette racine ; (2) ancrage au script — construire les chemins à partir de l'emplacement du fichier .py lui-même :
BASE_DIR = Path(__file__).parent # the folder containing THIS .py file
data_file = BASE_DIR / "data" / "notes.txt"
__file__ est le chemin du script en cours — un programme ancré ainsi marche quel que soit le dossier de lancement. C'est le motif des vrais outils.
b) Le piège des \ dans les chaînes. Sous Windows les chemins s'écrivent avec des \… mais dans une chaîne Python, \ est le caractère d'échappement (\n = saut de ligne, \t = tabulation). Écrire "C:\Users\jtron" explose ou se corrompt silencieusement (\U annonce un caractère Unicode !). Trois issues :
p = Path(r"C:\Users\jtron\notes.txt") # raw string : le r désactive les \
p = Path("C:/Users/jtron/notes.txt") # les / marchent AUSSI sous Windows
p = Path("C:") / "Users" / "jtron" # pathlib assemble, zéro backslash tapé
Dans ce cours : chemins littéraux avec / ou en raw string, assemblage avec l'opérateur / de pathlib.
c) Path sait presque tout faire. Les attributs et méthodes à connaître (tous utilisés dans les exercices) :
p = Path("C:/Users/jtron/Claude/learnpython/rapport.PDF")
p.name # 'rapport.PDF' nom avec extension
p.stem # 'rapport' nom sans extension
p.suffix # '.PDF' extension, AVEC le point, casse conservée !
p.parent # Path('C:/Users/jtron/Claude/learnpython')
p.exists() # le fichier/dossier existe-t-il ?
p.is_file() # est-ce un fichier ?
p.is_dir() # est-ce un dossier ?
p.resolve() # le chemin ABSOLU correspondant
Pour lire/écrire un petit fichier d'un coup, Path a même des raccourcis qui remplacent open() : p.read_text(encoding="utf-8") et p.write_text("contenu", encoding="utf-8"). La règle utf-8 s'applique là aussi — même défaut système, même piège qu'en 05-1.
d) mkdir et glob.
out = Path("sorties")
out.mkdir(exist_ok=True) # sans exist_ok : erreur si déjà là
deep = Path("a/b/c")
deep.mkdir(parents=True, exist_ok=True) # parents=True crée toute la chaîne
for txt in Path.cwd().glob("*.txt"): # tous les .txt du dossier
print(txt.name)
glob("*.txt") renvoie les chemins qui correspondent au motif ; * signifie « n'importe quoi ». glob("**/*.txt") descend aussi dans les sous-dossiers.
Difficulté honnête : le concept de dossier courant est simple à énoncer et long à intégrer — tu VAS retomber dans le piège, plusieurs fois, sous des déguisements différents. C'est normal. L'objectif réaliste de la leçon : qu'en voyant FileNotFoundError, ton PREMIER réflexe soit d'afficher Path.cwd() — pas de « vérifier que le fichier existe » pour la cinquième fois.
5. Exemples commentés
Exemple 1 — construire et inspecter un chemin (sans toucher au disque)
from pathlib import Path
p = Path("data") / "exports" / "report.txt"
print(p) # data\exports\report.txt (affiché avec les \ de Windows)
print(p.name) # report.txt
print(p.suffix) # .txt
print(p.parent) # data\exports
Créer un Path ne crée RIEN sur le disque : c'est juste une adresse, le fichier peut ne pas exister. Note que tu as tapé des / et que Windows affiche des \ — pathlib traduit.
Exemple 2 — où suis-je ? qui existe ?
from pathlib import Path
print(Path.cwd()) # le dossier courant du programme
print(Path("courses.txt").exists()) # True si lancé depuis le dossier de 05-1...
print(Path("courses.txt").resolve()) # ...et voici où Python le cherche VRAIMENT
resolve() est ton outil de diagnostic n°1 : il montre le chemin absolu que Python construit à partir de ton chemin relatif. S'il pointe ailleurs que prévu, le mystère est résolu.
Exemple 3 — créer un dossier et écrire dedans
from pathlib import Path
out = Path("sorties")
out.mkdir(exist_ok=True) # idempotent : relançable sans erreur
report = out / "rapport.txt"
report.write_text("Tout va bien.\n", encoding="utf-8")
print(report.read_text(encoding="utf-8")) # Tout va bien.
write_text / read_text : parfaits pour un contenu d'un bloc. Pour du ligne-à-ligne ou de l'ajout ("a"), on garde with open(...) — les deux mondes se combinent : with open(report, "a", encoding="utf-8") as f: fonctionne, un Path se donne directement à open().
Exemple 4 — lister et filtrer avec glob
from pathlib import Path
folder = Path.cwd()
for path in sorted(folder.glob("*.py")):
print(path.name)
Sortie (par exemple) :
ajoute.py
compteur.py
journal.py
glob renvoie des objets Path complets ; sorted() les met en ordre alphabétique (l'ordre de glob n'est pas garanti).
Exemple 5 — le script ancré, insensible au dossier de lancement
from pathlib import Path
BASE_DIR = Path(__file__).parent # folder of this script
data_file = BASE_DIR / "data.txt"
with open(data_file, encoding="utf-8") as f:
print(f.read().strip())
Ce script trouve SON data.txt (celui rangé à côté de lui) même si tu le lances depuis C:\ — parce que le chemin ne part plus du dossier courant mais de l'emplacement du .py. Compare avec open("data.txt") qui, lancé depuis C:\, chercherait C:\data.txt.
Exemple 6 — trier des fichiers par extension (avant-goût du projet 10)
from pathlib import Path
counts = {} # dict extension -> nombre (⏪ niveau 03)
for path in Path.cwd().iterdir(): # iterdir : TOUT le contenu du dossier
if path.is_file(): # on ignore les sous-dossiers
ext = path.suffix.lower() # '.TXT' et '.txt' comptent ensemble
counts[ext] = counts.get(ext, 0) + 1
print(counts) # {'.py': 3, '.txt': 4}
Trois détails de pro : is_file() pour écarter les dossiers, suffix.lower() car l'extension garde la casse d'origine, et .get(ext, 0) pour le premier passage (⏪ niveau 03).
6. Erreurs fréquentes
1) « Le fichier est LÀ, je le vois ! » — et pourtant FileNotFoundError
# lancé avec : cd C:\Users\jtron puis python Claude\learnpython\report.py
with open("data.txt", encoding="utf-8") as f: # cherche C:\Users\jtron\data.txt
...
Le fichier est bien dans learnpython, mais le chemin relatif part du dossier courant (C:\Users\jtron). Correction : lancer depuis le bon dossier, ou ancrer avec Path(__file__).parent. Diagnostic express : print(Path.cwd()) + print(Path("data.txt").resolve()).
2) Les \ nus dans une chaîne
p = "C:\Users\jtron\notes.txt"
# SyntaxError: (unicode error) 'unicodeescape' codec can't decode bytes
# in position 2-3: truncated \UXXXXXXXX escape
\U annonce un caractère Unicode à 8 chiffres — sers n'en est pas un. Vicieux : "C:\temp" ne fait AUCUNE erreur (\t = tabulation !) et cherche un chemin C:<TAB>emp. Correction : raw string r"C:\Users\...", ou des /, ou l'assemblage Path(...) / ....
3) Concaténer des chemins avec +
folder = "sorties"
path = folder + "rapport.txt" # 'sortiesrapport.txt' — séparateur oublié !
Pas d'erreur, mauvais fichier. Avec pathlib la question disparaît : Path("sorties") / "rapport.txt". Le / met le séparateur, toujours, le bon.
4) mkdir() qui échoue — deux cas distincts
Path("sorties").mkdir() # 2e lancement : FileExistsError
Path("a/b/c").mkdir() # si a/b n'existe pas : FileNotFoundError [WinError 3]
Deux erreurs différentes, deux options différentes : exist_ok=True rend le premier cas relançable, parents=True crée les intermédiaires du second. Le duo mkdir(parents=True, exist_ok=True) est le réglage « je veux ce dossier, débrouille-toi » — c'est celui des scripts robustes.
5) Oublier que suffix garde le point et la casse
if path.suffix == "jpg": # toujours faux : suffix vaut '.jpg'
if path.suffix == ".jpg": # rate photo.JPG
if path.suffix.lower() == ".jpg": # ✅ correct
Aucun message d'erreur — juste un filtre qui ne matche rien. Les deux oublis classiques du mini-projet de rangement.
6) read_text() sans encodage
content = Path("notes.txt").read_text() # encodage système : cp1252 ici !
Même piège qu'en 05-1, sous un déguisement pathlib. read_text et write_text prennent encoding="utf-8" — il est tout aussi obligatoire qu'avec open().
7. Lire les messages d'erreur
Le FileNotFoundError version mkdir — parent manquant :
Traceback (most recent call last):
File "C:\Users\jtron\Claude\learnpython\lessons\level-05-fichiers-exceptions-modules\exercices\mes-reponses\ex-05-2-c.py", line 3, in <module>
Path("x/y/z").mkdir()
~~~~~~~~~~~~~~~~~~~^^
File "C:\Users\jtron\AppData\Local\Python\pythoncore-3.14-64\Lib\pathlib\__init__.py", line 1011, in mkdir
os.mkdir(self, mode)
~~~~~~~~^^^^^^^^^^^^
FileNotFoundError: [WinError 3] The system cannot find the path specified: 'x\\y\\z'
Nouveauté : le traceback a deux étages. De bas en haut : la dernière ligne donne l'erreur (FileNotFoundError — ici « je ne peux pas créer z car x\y n'existe pas ») ; l'étage au-dessus est DANS pathlib (Lib\pathlib\__init__.py) — ce n'est pas ton code, tu ne le corrigeras pas ; on remonte encore : ex-05-2-c.py, ligne 3 — VOILÀ ta ligne. Règle générale : dans un traceback à étages, ton bug est dans la ligne la plus haute qui appartient à TES fichiers. Le 'x\\y\\z' du message n'est pas une bizarrerie : c'est un \ affiché en écriture échappée.
Et la SyntaxError des backslashes :
File "C:\Users\jtron\Claude\learnpython\lessons\level-05-fichiers-exceptions-modules\exercices\mes-reponses\ex-05-2-a.py", line 1
p = "C:\Users\jtron"
^^^^^^^^^^^^^^^^
SyntaxError: (unicode error) 'unicodeescape' codec can't decode bytes in position 2-3: truncated \UXXXXXXXX escape
Pas de Traceback (most recent call last) : comme en leçon 01-1, une SyntaxError frappe avant l'exécution — aucune ligne n'a tourné. unicodeescape + un chemin Windows dans la ligne pointée = diagnostic immédiat : backslash nu, il faut un r"..." ou des /.
8. Exercices — faciles
Règles : fichiers dans
lessons/level-05-fichiers-exceptions-modules/exercices/mes-reponses/, nommésex-05-2-a.py, etc. Prédis avant d'exécuter.
a) Sans toucher au disque : construis avec pathlib le chemin archives/2026/rapport-juillet.txt et affiche séparément le chemin complet, name, stem, suffix et parent. Prédis les cinq affichages AVANT de lancer (attention au point du suffixe).
b) Écris ou-suis-je.py qui affiche Path.cwd() et Path("courses.txt").resolve(). Lance-le DEUX fois : une fois depuis mes-reponses/, une fois depuis la racine du repo (python lessons/level-05-fichiers-exceptions-modules/exercices/mes-reponses/ou-suis-je.py). Note en commentaire ce qui change et pourquoi.
c) Écris un programme qui crée un dossier sorties/, y écrit bonjour.txt contenant Bonjour pathlib ! (via write_text), puis le relit et l'affiche (via read_text). Le programme doit être relançable sans erreur. utf-8 partout.
9. Exercices — moyens
a) Écris inventaire.py qui affiche, pour le dossier courant, tous les fichiers .py triés par nom (un par ligne), puis le nombre total. Utilise glob et sorted. Contrainte : les noms seuls (name), pas les chemins complets.
b) Ce script est rangé dans mes-reponses/ à côté de son config.txt (crée les deux). Il plante quand on le lance depuis la racine du repo :
with open("config.txt", encoding="utf-8") as f:
print(f.read())
Reproduis la panne, explique-la en commentaire, puis corrige avec Path(__file__).parent pour qu'il marche depuis N'IMPORTE quel dossier de lancement. Re-teste depuis les deux endroits.
c) Écris familles.py qui compte les fichiers du dossier courant par extension et affiche le bilan trié :
.py : 6
.txt : 4
(sans extension) : 1
Utilise iterdir(), is_file(), suffix.lower() et un dict (⏪ niveau 03). Piège prévu : certains fichiers n'ont pas d'extension (suffix vaut "").
10. Exercices — difficiles
a) Écris la fonction unique_destination(path) : elle reçoit un Path cible et renvoie un chemin qui n'existe pas encore — rapport.txt s'il est libre, sinon rapport_1.txt, sinon rapport_2.txt, etc. Teste-la en créant à la main les fichiers qui « gênent ». (C'est un morceau du vrai mini-projet 10 — écris-la bien, tu la réutiliseras telle quelle.)
- Indice 1 : il te faut le nom sans extension (
stem), l'extension (suffix) et le dossier (parent) — tout est dans la section 4. - Indice 2 : une boucle
whiletant que le candidatexists(). - Indice 3 : le candidat numéro
nse construit avecpath.parent / f"{path.stem}_{n}{path.suffix}".
11. Mini-projet lié — « rangement de fichiers, version simulation »
C'est LE projet pathlib du cours : projects/mini-projects/10-script-rangement-fichiers.md — un script qui classe un dossier en désordre par type de fichier, avec mode simulation par défaut.
Pour cette leçon, réalise la version simulation seule (étapes 1, 3 et 4 de la fiche, ~30–40 min) : créer un test_folder/ de faux fichiers (Path(...).touch()), classer chaque fichier par extension via un dict CATEGORIES, et AFFICHER [SIMULATION] photo.jpg -> images/ sans rien déplacer. Aucun risque, tout le cœur pathlib y est. Tu feras la version --apply (déplacement réel + collisions avec ta fonction unique_destination) après la leçon 05-3 — on ne touche pas aux vrais fichiers sans savoir gérer les erreurs.
12. Correction / méthode de correction
Les solutions détaillées sont dans solutions/serie-05-2-solutions.md.
Méthode : (1) finis TOUS les exercices avant d'ouvrir les solutions ; (2) compare le raisonnement ; (3) vérifie ton réflexe diagnostic — as-tu affiché cwd()/resolve() au premier FileNotFoundError, ou as-tu « cherché le fichier » ? ; (4) une ligne de journal par écart ; (5) refais de tête le lendemain.
13. À retenir
- Un chemin relatif part du dossier courant du terminal, pas du dossier du script. C'est la cause n°1 des
FileNotFoundError. - Diagnostic express :
print(Path.cwd())etprint(p.resolve()). - Script robuste : ancrer avec
BASE_DIR = Path(__file__).parent. - Assembler des chemins : l'opérateur
/depathlib— jamais+. \dans une chaîne = échappement : raw stringr"C:\..."ou des/.p.name/p.stem/p.suffix(avec point ! penser.lower()) /p.parent.mkdir(parents=True, exist_ok=True): le duo des scripts relançables.read_text/write_text: pratique, maisencoding="utf-8"reste obligatoire.
14. Questions de révision
- Ton script marche dans VS Code mais plante dans le terminal avec
FileNotFoundError. Quelle est l'explication la plus probable, et quelles sont les DEUX lignes de diagnostic à ajouter ? - Quelle est la différence entre un chemin relatif et un chemin absolu, et lequel
resolve()renvoie-t-il ? - Pourquoi
"C:\temp\notes.txt"est-il piégé alors qu'il ne lève parfois aucune erreur ? - Que vaut
Path("photo.JPG").suffix— exactement ? Et comment filtrer les.jpgsans rater celui-là ? - Quelle est la différence entre
FileExistsErroretFileNotFoundErrorlors d'unmkdir(), et quelle option résout chacun ? - Que fait
Path(__file__).parentet quel problème ce motif résout-il ? Path("data") / "notes.txt": que fabrique cette expression, et le fichier doit-il exister ?- Dans un traceback qui traverse
pathlib\__init__.py, où se trouve TON bug ?
15. Checklist de compréhension
- Je peux expliquer « dossier courant » à quelqu'un d'autre, avec l'exemple d'un script lancé de deux endroits.
- Je sais construire un chemin multi-dossiers avec
/sans taper un seul backslash. - Je sais extraire nom, extension et dossier d'un
Pathsans regarder la leçon. - Devant un
FileNotFoundError, mon premier réflexe estcwd()+resolve(). - Je sais rendre un
mkdirrelançable et créer une arborescence complète. - J'ai écrit
unique_destination()et je l'ai testée avec des collisions réelles. - J'ai fait la version simulation du projet de rangement et chaque fichier part dans la bonne catégorie.
Si une case reste vide : refais les exercices de la section correspondante demain, AVANT d'ouvrir la leçon suivante.
16. Commit conseillé
git add lessons/level-05-fichiers-exceptions-modules/exercices/mes-reponses/
git commit -m "exercises: complete lesson 05-2 paths with pathlib"
Puis, si c'est ta fin de session : entrée de journal + git push.