Apprenez en lisant dans l'ordre

logging — Enregistrer au lieu de print

Les 5 niveaux de log et basicConfig, les codes de format comme %(levelname)s, et la sortie fichier via FileHandler pour abandonner le débogage à coups de print.

logging est le module standard pour enregistrer le comportement de ton app par étapes. Écrire des sorties de débogage avec print rencontre des problèmes : tu oublies de les retirer en production, tu ne peux pas dire ce qui est important, et passer à une sortie fichier plus tard signifie tout réécrire. Avec logging, tu obtiens des niveaux d'importance (DEBUG / INFO / WARNING / ERROR / CRITICAL) pour filtrer la sortie, Formatter pour un formatage unifié, et Handler pour basculer les destinations (stdout / fichier / distant).

Comment logging traite un appel
Codelogger.info(...)LoggerFiltre par niveauHandlerChoisit destinationFormatterAssemble le format
Quand tu appelles logger.info(...) depuis ton code, le flux est Logger filtre par niveauHandler décide de la destinationFormatter assemble le format. Comme les trois responsabilités sont séparées, changer le format ou la destination à un seul endroit change tout le comportement.

Niveaux de log — 5 paliers d'importance

logging a 5 niveaux, et seuls les messages au niveau fixé sur le Logger ou au-dessus sont émis. La pratique standard est DEBUG pendant le développement et INFO ou WARNING en production — et pouvoir changer le volume de sortie depuis un seul réglage sans toucher au code est la plus grande différence avec print.

Les 5 niveaux de logging
DEBUGDétailINFOProgressionWARNINGAttentionERRORÉchecCRITICALFatal
DEBUG (détail) / INFO (progression normale) / WARNING (attention) / ERROR (échec) / CRITICAL (catastrophique). Seuls les messages au niveau du Logger ou au-dessus sont émis — DEBUG en développement, INFO/WARNING en production est le motif standard.

D'abord, configure le formatage et le niveau avec basicConfig et émets un log INFO.

① Importe logging et configure basicConfig avec level=logging.INFO, format="[%(levelname)s] %(message)s", force=True

② Récupère le logger via logger = logging.getLogger("app")

③ Émets un log via logger.info("App démarrée")

(Si ton code s'exécute correctement, l'explication apparaîtra.)

Éditeur Python

Exécuter le code pour voir le résultat

Choisir entre WARNING / ERROR / DEBUG

Avec le logger configuré, appelle des niveaux autres que INFO et observe la sortie. Comme le niveau est fixé à INFO, DEBUG ne produit aucune sortie du tout — confirme aussi ça. C'est la fondation du comportement "changer le volume de sortie depuis un réglage" de logging.

Réutilise la config du logger de la Pratique 1 et émets à trois niveaux. Note : à INFO, DEBUG n'apparaît pas.

① Émets WARNING via logger.warning("Fichier de config obsolète")

② Émets ERROR via logger.error("Échec de connexion à la BD")

③ Appelle logger.debug("Trace détaillée") — au niveau INFO, rien ne devrait s'afficher

Éditeur Python

Exécuter le code pour voir le résultat

Émettre des logs vers un fichier

Les projets réels ont presque toujours besoin que les logs persistent dans un fichier, pas seulement à l'écran. Plus tard tu voudras grep pour une investigation post-mortem ou les ingérer dans un outil de monitoring. Passe filename="app.log" à basicConfig et la destination du root logger bascule vers un fichier — faire la même chose avec print signifierait réécrire chaque appel print, mais avec logging tu changes une ligne de config pour rediriger.

Sortie écran vs sortie fichier
logger.info(...)(écran seulement)Le terminal se ferme→ Logs perduslogger.info(...)filename=app.loggrep ultérieur / ingestionoutils de monitoring
La sortie écran disparaît quand le terminal se ferme, mais la sortie fichier survit pour un grep post-mortem et une ingestion par outil de monitoring. Avec logging, tu bascules entre les deux en une ligne de config.

filemode est soit "a" (par défaut, append) soit "w" (overwrite). Append est le choix de production — garde l'historique. Overwrite est souvent plus pratique pendant le développement — repart à zéro à chaque fois. Pour émettre à la fois à l'écran et au fichier, tu sautes basicConfig et combines explicitement StreamHandler + FileHandler (couvert plus loin).

Combinaisons filemode et Handler
filemode="a" append→ Garde historique (prod)filemode="w" overwrite→ Tabula rasa (dev)StreamHandler→ Sortie écranFileHandler→ Sortie fichier
filemode="a" ajoute et garde l'historique (production), tandis que filemode="w" écrase pour repartir à zéro à chaque exécution (développement). Pour envoyer à la fois à l'écran et au fichier, construis StreamHandler et FileHandler séparément et addHandler pour chacun.
import logging
import os

# Crée d'abord le dossier parent (FileHandler ne le crée pas automatiquement)
os.makedirs("logs", exist_ok=True)

# Passe filename à basicConfig pour basculer la destination vers un fichier
logging.basicConfig(
    level=logging.INFO,
    format="[%(levelname)s] %(message)s",
    filename="logs/app.log",   # Sortie fichier
    filemode="w",               # "a" (append) ou "w" (overwrite)
    force=True,
)
logger = logging.getLogger("app")

logger.info("Démarré")
logger.warning("Config obsolète")
logger.error("Échec de connexion à la BD")

# Relit le fichier pour confirmer le contenu
with open("logs/app.log") as f:
    print(f.read(), end="")

Passe filename à basicConfig pour basculer les logs vers un fichier, puis relis le contenu depuis le disque.

① Utilise os.makedirs("logs", exist_ok=True) pour créer d'abord le dossier parent (FileHandler ne le crée pas automatiquement)

② Importe logging et appelle basicConfig avec level=logging.INFO, format="[%(levelname)s] %(message)s", filename="logs/app.log", filemode="w", force=True

③ Récupère le logger avec logger = logging.getLogger("app")

④ Émets 3 logs en INFO / WARNING / ERROR avec les messages "Démarré", "Config obsolète", "Échec de connexion à la BD"

⑤ Ouvre logs/app.log en lecture, puis sous le titre --- contenu de logs/app.log --- affiche le contenu sans saut de ligne final (print(content, end=""))

Éditeur Python

Exécuter le code pour voir le résultat

Définir les handlers dans un fichier séparé

À mesure que ton app grandit, extrais la config du logger (format, niveau, destination) dans un module dédié et fais que chaque module l'import. Les configs de logger ne se dupliquent plus, et tu changes le format/destination à un seul endroit. Le motif : construire un StreamHandler directement, attacher un Formatter, enregistrer avec logger.addHandler(...) — tout ça vit dans un fichier séparé.

Centraliser log_setup ; les modules importent depuis lui
log_setup.py(format, destination, niveau)main.pyorders.pyusers.pyimportimportimport
log_setup.py est la source unique pour la config du logger (format, destination, niveau). Chaque module (main.py / orders.py / users.py) appelle juste setup_logger(__name__) pour obtenir un logger configuré. Pour changer le format ou la destination, édite seulement log_setup.py.
# log_setup.py — module dédié à la config du logger
import logging

def setup_logger(name):
    logger = logging.getLogger(name)
    logger.setLevel(logging.INFO)

    if not logger.handlers:                       # Empêche l'enregistrement en double
        handler = logging.StreamHandler()
        handler.setFormatter(
            logging.Formatter("[%(name)s] [%(levelname)s] %(message)s")
        )
        logger.addHandler(handler)
        logger.propagate = False                  # Ne propage pas au root
    return logger

# main.py — importe log_setup et l'utilise
from log_setup import setup_logger

logger = setup_logger("orders")
logger.info("Commande reçue")

log_setup.py est fourni (ouvre le panneau de fichiers 📂 à gauche de la console pour l'inspecter). Depuis main.py, importe setup_logger et émets 3 logs avec le logger nommé "orders".

① Importe la fonction via from log_setup import setup_logger

② Récupère un logger avec logger = setup_logger("orders")

③ Émets 3 logs en INFO / WARNING / ERROR : "Commande reçue", "Stock faible", "L'API de paiement a retourné une erreur"

Éditeur Python

Exécuter le code pour voir le résultat

Format plus détaillé — horodatage / module / ligne

La chaîne de format de Formatter accepte des codes de remplacement style %(...)s, te permettant d'empaqueter les infos opérationnellement pertinentes sur une seule ligne. Voici les six codes les plus utilisés avec exemples. Les noms de logger peuvent être hiérarchiques via la forme séparée par points comme parent.child.grandchild — appeler getLogger(__name__) depuis chaque module enregistre automatiquement "quel module a émis ce log".

CodeCe qu'il sortExemple
%(asctime)sHorodatage2024-12-01 10:30:45
%(name)sNom du logger (séparé par points pour la hiérarchie)app.orders
%(levelname)sNiveau du logINFO / WARNING / ERROR
%(funcName)sNom de la fonction appelanteprocess_order
%(lineno)dNuméro de ligne appelant42
%(message)sCorps du messageTraitement de la commande
import logging

# Format détaillé pour la production
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(name)s] [%(levelname)s] %(funcName)s:%(lineno)d - %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
    force=True,
)

logger = logging.getLogger("app.orders")

def process_order(order_id):
    logger.info(f"Traitement de la commande {order_id}")

process_order(1234)
# Exemple de sortie :
# 2024-12-01 10:30:45 [app.orders] [INFO] process_order:13 - Traitement de la commande 1234

Utilise 3 noms de logger hiérarchiques — app / app.orders / app.orders.payment — et montre de quel module vient chaque log avec %(name)s. En production tu ajouterais %(asctime)s pour l'horodatage, mais ici on s'en tient à la hiérarchie seule pour que la sortie reste déterministe.

① Importe logging et configure basicConfig avec level=logging.INFO, format="[%(name)s] [%(levelname)s] %(message)s", force=True

② Récupère 3 loggers : logging.getLogger("app"), logging.getLogger("app.orders"), logging.getLogger("app.orders.payment")

③ Émets un log depuis chacun : app appelle info("App démarrée"), app.orders appelle info("Commande reçue"), app.orders.payment appelle error("Erreur API de paiement")

Éditeur Python

Exécuter le code pour voir le résultat

Extraire la config dans un fichier yaml

Une fois que la configuration du logger devient complexe, au lieu de coder en dur format / handler / niveau dans Python, extrais-les dans un fichier de config yaml. logging.config.dictConfig(...) accepte une config en dict, donc parse simplement le yaml et passe-le pour configurer toute l'installation du logger. Ta base de code gère soudainement "des fichiers yaml différents pour production / staging / développement" sans changements.

Flux fichier de config yaml → dictConfig
logging.ymlformat / handler / niveauyaml.safe_load()+ dictConfig()Logger configurélogger.info(...)
Écris format / handler / niveau dans logging.yml, puis convertis-le en dict via yaml.safe_load() et passe à logging.config.dictConfig(...). Le côté Python charge juste le yaml et appelle dictConfig, et le logger est configuré.
# logging.yml — single source for logger config
version: 1
disable_existing_loggers: false
formatters:
  default:
    format: "[%(name)s] [%(levelname)s] %(message)s"
handlers:
  console:
    class: logging.StreamHandler
    formatter: default
    level: INFO
loggers:
  app:
    level: INFO
    handlers: [console]
    propagate: false
CléRôleNotes
versionVersion du schéma dictConfigActuellement seulement 1. Requis.
disable_existing_loggersDésactiver ou non les loggers existantsfalse recommandé (true fait taire les loggers comme app.orders d'avant)
formattersListe nommée de FormattersDéfinis sous n'importe quel nom (ex. default), référencé depuis handlers
formatters.<name>.formatChaîne de format (codes %(...)s)Identique à l'argument Formatter dans le code
handlersListe nommée de Handlers (destinations)Définis console / file / mail et ainsi de suite
handlers.<name>.classNom de classe Handler complètement qualifiélogging.StreamHandler / logging.FileHandler / logging.handlers.RotatingFileHandler etc.
handlers.<name>.formatterNom du Formatter à appliquerUtilise une clé définie sous formatters
handlers.<name>.levelNiveau par HandlerFiltre la sortie plus finement qu'au niveau Logger
loggersListe nommée de Loggersloggers.app est récupéré via logging.getLogger("app")
loggers.<name>.handlersListe de noms de Handlers attachés à ce loggerPlusieurs OK, comme [console, file]
loggers.<name>.propagatePropagation aux loggers parents ou nonfalse arrête la propagation au root logger (évite la double sortie)

logging.yml est fourni (ouvre le panneau de fichiers 📂 pour l'inspecter). Parse en dict via yaml.safe_load, puis passe à logging.config.dictConfig pour configurer le logger.

① Importe logging, logging.config, et yaml

② Ouvre avec with open("logging.yml") as f : et convertis en dict via yaml.safe_load(f)

③ Passe le dict à logging.config.dictConfig(...) pour appliquer

④ Récupère le logger via logger = logging.getLogger("app") et émets 1 INFO et 1 WARNING : "Démarré avec config yaml" et "WARNING via yaml"

Éditeur Python

Exécuter le code pour voir le résultat

Rotation des logs

Si les logs continuent de croître, le fichier gonfle indéfiniment, donc la production a besoin de rotationrenommer les vieux fichiers en un nom séparé et les supprimer éventuellement. logging.handlers propose deux variantes — basée sur la taille et basée sur le temps — et tu choisis selon le cas d'usage.

Deux types : basée sur la taille et basée sur le temps
RotatingFileHandlerBasée sur la taille(rotation quand maxBytes dépassé)TimedRotatingFileHandlerBasée sur le temps(rotation aux frontières when)
Basée sur la taille convient aux logs de débogage où tu veux un plafond strict sur l'utilisation disque. Basée sur le temps convient aux logs d'accès que tu veux découper par date pour analyse ou outils de monitoring.

RotatingFileHandler — basée sur la taille

RotatingFileHandler(filename, maxBytes, backupCount) est un Handler qui bascule vers un nouveau fichier quand maxBytes serait dépassé. Les vieux fichiers sont renommés avec un suffixe numérique (app.log.1 / app.log.2), et une fois backupCount dépassé le plus ancien est supprimé. Avec maxBytes=10_000_000 (10 Mo) + backupCount=5, tes logs cyclent dans un plafond de 60 Mo.

Comment RotatingFileHandler fonctionne
app.logÉcrituremaxBytes dépassé→ rotationapp.log.1app.log.2 ...(backups)Au-delà de backupCount→ Plus ancien supprimé
Quand le fichier actif app.log approche maxBytes, il est renommé app.log → app.log.1, et un nouveau app.log est créé pour continuer à écrire. Si app.log.N existe déjà, il est décalé en app.log.N → app.log.N+1, et le fichier le plus ancien au-delà de backupCount est supprimé.
import logging
from logging.handlers import RotatingFileHandler

# Basée sur la taille : rotation quand maxBytes est dépassé
size_handler = RotatingFileHandler(
    "app.log",
    maxBytes=10_000_000,   # Rotation quand ce serait dépassé (10 Mo)
    backupCount=5,         # Conserve app.log.1 à app.log.5 (max 60 Mo total)
)
size_handler.setFormatter(logging.Formatter("[%(levelname)s] %(message)s"))

rotate_logging.yml est fourni (ouvre le panneau de fichiers 📂 pour l'inspecter). La config RotatingFileHandler (maxBytes=60 / backupCount=2) vit dans yaml, et le code la charge et l'utilise simplement.

① Importe os / shutil, supprime et recrée le dossier rotate/. Vide aussi tous les handlers du logger rotate_demo avec close + removeHandler (évite les interférences à la ré-exécution)

② Ouvre le yaml avec with open("rotate_logging.yml"), convertis via yaml.safe_load(f), et passe à logging.config.dictConfig(...) pour configurer le handler

③ Récupère logger = logging.getLogger("rotate_demo") et émets 15 logs en boucle : for i in range(15): logger.info(f"event {i:02d}")

④ Depuis os.listdir("rotate"), prends seulement les fichiers commençant par app.log, trie-les, et sous le titre Fichiers après rotation : ◯ affiche chaque nom de fichier sous la forme - nom_fichier sur sa propre ligne

Éditeur Python

Exécuter le code pour voir le résultat

TimedRotatingFileHandler — basée sur le temps

TimedRotatingFileHandler(filename, when, interval, backupCount) rotate à la frontière temporelle spécifiée par when. Utilise when="midnight" (chaque jour à 0h00), "H" (par heure), "M" (minute), "S" (seconde), "D" (jour), et ainsi de suite. Les backups portent des suffixes d'horodatage comme app.log.2024-12-01_00-00-00, donc le nom de fichier te dit "quand ce log a été écrit" directement.

Comment TimedRotatingFileHandler fonctionne
app.logÉcritureProchain when (temps)→ rotationapp.log.2024-12-01app.log.2024-12-02(suffixe horodatage)Au-delà de backupCount→ Plus ancien supprimé
À l'heure spécifiée par when (chaque jour à 0h00, etc.), le app.log actif est renommé avec un nom suffixé d'horodatage (ex. app.log.2024-12-01_00-00-00) et un nouveau app.log s'ouvre. Les fichiers horodatés plus anciens au-delà de backupCount sont auto-supprimés.
import logging
from logging.handlers import TimedRotatingFileHandler

# Basée sur le temps : rotation chaque jour à minuit
day_handler = TimedRotatingFileHandler(
    "app.log",
    when="midnight",   # "S" sec / "M" min / "H" heure / "D" jour / "midnight" etc.
    interval=1,         # Tous les N unités (when="H", interval=6 signifie toutes les 6 heures)
    backupCount=30,     # Conserve 30 jours d'historique
)
day_handler.setFormatter(
    logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")
)

time_logging.yml est fourni. La config TimedRotatingFileHandler (when : S (par seconde) / interval : 1 / backupCount : 3) vit dans yaml, et le code Python utilise time.sleep pour simuler le passage du temps afin de déclencher les rotations.

① Importe logging / os / shutil / time. Vide tous les handlers du logger trotate_demo avec close + removeHandler, puis supprime et recrée le dossier trotate/

② Ouvre time_logging.yml, convertis en dict via yaml.safe_load(f), et passe à logging.config.dictConfig(...) pour configurer

③ Récupère le logger et émets 3 logs avec sleep entre eux : logger.info(...)time.sleep(1.2)logger.info(...)time.sleep(1.2)logger.info(...), déclenchant 2 rotations

④ Depuis os.listdir("trotate"), sépare app.log (log courant) de tout le reste (backups horodatés), puis affiche Log courant présent : True/False et Nombre de backups : ◯

Éditeur Python

Exécuter le code pour voir le résultat
QUIZ

Vérification des connaissances

Répondez à chaque question une par une.

Question 1Avec logging.basicConfig(level=logging.INFO), quel niveau est supprimé ?

Question 2Quand tu passes filename="app.log" à logging.basicConfig, où vont les logs ?

Question 3Quel est le bénéfice principal d'extraire la config du logger (format / niveau / handler) dans un module séparé ?