Tom Daluzeau
← Parcours Python · système
Python · système · leçon 2 / 8Ch. 4 · Python et le système13 minTerminée

Rapports datés, os.scandir et shutil

Créer un dossier de rapports, nommer chaque rapport avec sa date pour qu'il se trie tout seul, lister un dossier avec os.scandir, calculer l'âge d'un fichier, puis copier, déplacer, archiver et supprimer avec shutil.

Mis à jour le

À recouper avec le cours. Cette leçon a été écrite à partir du plan du chapitre, avant relecture avec les supports. En cas de doute, le cours fait foi.

Pourquoi on voit ça

Ton outil vérifie le parc tous les jours et écrit un rapport à chaque passage. Très vite, tu as des dizaines de fichiers. Il faut les nommer pour retrouver le plus récent sans réfléchir, les lister, repérer ceux qui sont trop vieux, puis les archiver ou les supprimer.

C’est le quotidien du DevOps : rotation de logs, sauvegardes, nettoyage. Python le fait avec trois modules de base : os, datetime et shutil. La leçon se termine par la fonction la plus dangereuse du chapitre.

Un dossier de rapports : os.makedirs

Avant d’écrire un rapport, le dossier doit exister.

import os

os.makedirs("reports/2026", exist_ok=True)
os.makedirs("reports/2026", exist_ok=True)
print("ok")
ok

Deux choses à savoir :

  • makedirs crée aussi les dossiers intermédiaires : ici reports, puis 2026 dedans.
  • exist_ok=True veut dire « si le dossier existe déjà, ce n’est pas grave ». Sans lui, le deuxième appel plante.
os.makedirs("reports/2026")
FileExistsError: [Errno 17] File exists: 'reports/2026'

Un outil relancé tous les jours trouve le dossier déjà là dès le deuxième jour. Il lui faut donc toujours exist_ok=True.

Dater un rapport : datetime

datetime.now() donne la date et l’heure du moment. Pour les exemples, on fixe une date à la main.

from datetime import datetime

d = datetime(2026, 11, 14, 9, 5)
print(d.strftime("%Y-%m-%d"))
print(d.strftime("%Y-%m-%d_%H-%M"))
print(d.isoformat())
2026-11-14
2026-11-14_09-05
2026-11-14T09:05:00

strftime transforme une date en texte selon un format que tu choisis :

Code Sens Exemple
%Y année sur 4 chiffres 2026
%m mois sur 2 chiffres 11
%d jour sur 2 chiffres 04
%H heure sur 2 chiffres 09
%M minutes sur 2 chiffres 05

Attention à la casse : %m est le mois, %M les minutes.

isoformat() ne prend pas de format : il écrit toujours AAAA-MM-JJ, un T, puis l’heure. C’est la forme standard pour une date dans un rapport JSON.

Le nom du rapport se construit avec une f-string :

nom = f"reports/report-{d.strftime('%Y-%m-%d')}.json"
print(nom)
reports/report-2026-11-14.json
report- 2026 - 11 - 14 .json %Y %m %d texte fixe extension f"report-{d.strftime('%Y-%m-%d')}.json" du plus gros au plus petit : année, mois, jour, toujours avec leurs zéros
strftime remplit les trois morceaux datés ; le reste du nom est du texte fixe.

Pourquoi AAAA-MM-JJ se trie tout seul

sorted et ls trient les noms comme du texte : caractère par caractère, de gauche à droite. Si le nom commence par l’année, puis le mois, puis le jour, l’ordre alphabétique est aussi l’ordre du temps.

iso = ["report-2026-11-02.json", "report-2026-10-30.json", "report-2025-12-25.json"]
fr = ["report-02-11-2026.json", "report-30-10-2026.json", "report-25-12-2025.json"]
print(sorted(iso))
print(sorted(fr))
['report-2025-12-25.json', 'report-2026-10-30.json', 'report-2026-11-02.json']
['report-02-11-2026.json', 'report-25-12-2025.json', 'report-30-10-2026.json']

Première ligne : du plus ancien au plus récent. Le dernier rapport est simplement sorted(iso)[-1]. Deuxième ligne : le tri regarde d’abord le jour, et l’ordre ne veut plus rien dire.

AAAA-MM-JJ report-2025-12-25.json 25 déc. 2025 report-2026-10-30.json 30 oct. 2026 report-2026-11-02.json 2 nov. 2026 [-1] : le plus récent JJ-MM-AAAA report-02-11-2026.json 2 nov. 2026 report-25-12-2025.json 25 déc. 2025 report-30-10-2026.json 30 oct. 2026 [-1] : pas le plus récent
sorted compare de gauche à droite. Si le nom commence par l'année, l'ordre du texte est celui du temps.

Les zéros comptent aussi : "2026-9-5" se range après "2026-10-1", car le caractère 9 est plus grand que 1. strftime écrit toujours 09 et 05, ce qui évite le piège.

Lister un dossier : os.scandir

os.scandir(dossier) donne une entrée par élément du dossier. Chaque entrée porte trois informations utiles :

Écriture Ce que ça donne
entry.name le nom seul, sans le dossier devant : "a.json"
entry.is_file() True pour un fichier, False pour un dossier
entry.stat().st_mtime la date de dernière modification

Avec un dossier reports qui contient a.json, b.json, notes.txt et un sous-dossier old :

import os

noms = []
for entry in os.scandir("reports"):
    if entry.is_file() and entry.name.endswith(".json"):
        noms.append(entry.name)
print(sorted(noms))
['a.json', 'b.json']

old est écarté par is_file(), notes.txt par endswith. L’ordre dans lequel scandir donne les entrées n’est pas garanti : si l’ordre compte, trie avec sorted.

reports/ a.json b.json notes.txt old/ is_file() endswith(".json") dans noms True True True True True False False pas testé print(sorted(noms)) → ['a.json', 'b.json']
Chaque entrée passe les deux tests du if. Le dossier old s'arrête au premier, notes.txt au second.

Note bien les parenthèses : name est une valeur, is_file() et stat() sont des appels.

L’âge d’un fichier

st_mtime n’est pas une date lisible. C’est un nombre de secondes écoulées depuis le 1er janvier 1970. time.time() donne l’instant présent dans la même unité. L’âge d’un fichier est donc une simple soustraction :

import time

age_secondes = time.time() - entry.stat().st_mtime
age_jours = age_secondes / 86400

Un jour dure 86 400 secondes (24 × 60 × 60). Avec des nombres fixes, pour voir le calcul :

maintenant = 1_800_000_000
mtime = maintenant - 3 * 86400
print((maintenant - mtime) / 86400)
3.0

La rotation des rapports tient alors en une condition : if age_jours > 7:.

0 1er janv. 1970 7 jours avant plus de 7 jours : à archiver récent : on garde b.json c.json a.json time.time() maintenant âge de b.json = maintenant − st_mtime = 864 000 s = 10 jours
st_mtime et time.time() sont deux points sur la même ligne, en secondes. L'âge est l'écart entre les deux.

Copier, déplacer, archiver, supprimer : shutil

Fonction Ce qu’elle fait La source existe encore ?
shutil.copy2(src, dst) copie le fichier et ses métadonnées (dont la date de modification) oui
shutil.move(src, dst) déplace ou renomme non
shutil.make_archive(base, "zip", dossier) compresse un dossier dans base.zip oui
shutil.rmtree(dossier) supprime le dossier et tout son contenu non
import shutil

shutil.copy2("reports/a.json", "a.bak")
shutil.move("reports/b.json", "archive/b.json")
shutil.make_archive("backup", "zip", "reports")

(Le dossier archive doit déjà exister : crée-le avec os.makedirs.) Trois détails qui tombent en QCM :

  • copy2 garde la date. La copie a le même st_mtime que l’original. Un calcul d’âge sur la copie donne donc le même résultat.
  • make_archive ajoute l’extension. Tu écris "backup", tu obtiens backup.zip. Avec "gztar" à la place de "zip", tu obtiens backup.tar.gz.
  • make_archive ne supprime rien. Le dossier reports est toujours là après.

rmtree : le danger

shutil.rmtree("reports") est l’équivalent de rm -rf reports. Il supprime le dossier, ses fichiers et ses sous-dossiers :

  • sans confirmation ;
  • sans corbeille : rien n’est récupérable ;
  • quel que soit le contenu.

Le risque réel, c’est le chemin. Une variable vide ou mal construite, et tu effaces autre chose que prévu. Avant un rmtree, vérifie le chemin (affiche-le, ou journalise-le), et ne le construis jamais à partir d’une valeur que tu ne contrôles pas.

À retenir

  • os.makedirs(chemin, exist_ok=True) crée le dossier et ses parents, sans planter s’il existe déjà. Sans exist_ok, c’est FileExistsError.
  • strftime("%Y-%m-%d") fabrique un nom daté ; isoformat() donne 2026-11-14T09:05:00.
  • Un nom en AAAA-MM-JJ se trie tout seul : l’ordre alphabétique est l’ordre des dates.
  • os.scandir donne des entrées : entry.name, entry.is_file(), entry.stat().st_mtime.
  • Âge d’un fichier en secondes : time.time() - st_mtime. Un jour = 86 400 secondes.
  • copy2 copie avec les métadonnées, move déplace, make_archive compresse, rmtree supprime tout, sans confirmation ni corbeille.

Entraînement

12 questions, dont 3 à compléter. Tu réponds, on passe à la suivante. La note arrive à la fin.

Cartes · 8

Réviser les cartes ici

Espace : retourner