logging (stdlib)
Le module de journalisation livré avec Python. Pas le plus sexy, mais c'est le standard : toutes les bibliothèques l'utilisent, et c'est ce qu'on attend de toi en formation et en entretien.
À quoi ça sert
Quand ton script grossit, print() ne suffit plus : tu veux
distinguer ce qui est une info, un avertissement ou une
erreur, écrire dans un fichier en plus de la console, ou désactiver
les messages de debug en production sans toucher au code. C'est exactement
le rôle de logging.
- Niveaux de gravité : tu écris du DEBUG partout, et en prod tu n'affiches que les WARNING et au-dessus.
- Plusieurs destinations : console, fichier rotatif, syslog, email d'alerte… simultanément.
- Format configurable : timestamp, nom du module, numéro de ligne, exception, le tout dans une chaîne au choix.
- Hiérarchique : chaque module a son propre logger, et une config centrale les pilote tous.
print() ?
print() écrit toujours sur stdout, sans timestamp, sans
niveau, sans contexte. Si ton script tombe à 3h du matin, tu retrouves
« erreur » dans les logs et tu ne sais ni quand, ni où, ni
pourquoi. logging ajoute toutes ces métadonnées
automatiquement, et permet de couper le bruit en changeant un seul
paramètre.
Les 3 concepts à connaître
-
Logger — l'objet sur lequel tu appelles
.info(),.error(), etc. Chaque module a le sien, identifié par un nom (souvent__name__). Les loggers sont organisés en arbre :app.api.routesest enfant deapp.api, lui-même enfant deapp, lui-même enfant du root logger. -
Handler — la destination.
StreamHandlerécrit sur la console,FileHandlerdans un fichier,RotatingFileHandlerdans un fichier qui se renouvelle quand il devient trop gros. Un logger peut avoir plusieurs handlers. -
Formatter — le gabarit qui transforme un message en
ligne lisible. Ex.
%(asctime)s [%(levelname)s] %(name)s: %(message)sdonne2026-05-11 14:32:01 [INFO] app.api: serveur démarré.
Du plus bas au plus haut : DEBUG (verbeux, dev seulement),
INFO (étapes normales), WARNING (anomalie qui
n'arrête pas), ERROR (échec d'une opération),
CRITICAL (l'appli ne peut plus fonctionner). Le logger
affiche tout ce qui est ≥ son niveau configuré — mettre
INFO coupe le DEBUG sans toucher au code.
Un exemple d'usage
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
)
logger = logging.getLogger(__name__)
logger.info("démarrage de l'application")
logger.warning("clé manquante, valeur par défaut utilisée")
try:
x = 1 / 0
except ZeroDivisionError:
logger.exception("division impossible") # inclut la stack trace
Sortie typique :
2026-05-11 14:32:01 [INFO] __main__: démarrage de l'application
2026-05-11 14:32:01 [WARNING] __main__: clé manquante, valeur par défaut utilisée
2026-05-11 14:32:01 [ERROR] __main__: division impossible
Traceback (most recent call last):
File "app.py", line 12, in <module>
x = 1 / 0
ZeroDivisionError: division by zero
How-to : utiliser logging proprement
-
Toujours nommer ton logger
La règle d'or : dans chaque fichier, fais
logger = logging.getLogger(__name__). Le nom correspond automatiquement au chemin du module (app.api.routes), ce qui permet de filtrer par module dans la config.python# app/api/routes.py import logging logger = logging.getLogger(__name__) def handler(): logger.info("requête reçue") -
Configurer une seule fois, au point d'entrée
La configuration se fait une seule fois, en haut de ton
main.pyou dans un module dédié. Les autres fichiers se contentent de récupérer leur logger avecgetLogger(__name__).python# main.py import logging logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) -
Logger dans un fichier qui tourne
RotatingFileHandlerest l'outil standard : un fichier qui se renouvelle quand il atteint une taille donnée, avec un historique limité.pythonimport logging from logging.handlers import RotatingFileHandler handler = RotatingFileHandler( "app.log", maxBytes=5 * 1024 * 1024, # 5 Mo max backupCount=3, # garde 3 anciens fichiers ) handler.setFormatter(logging.Formatter( "%(asctime)s [%(levelname)s] %(name)s: %(message)s" )) logging.getLogger().addHandler(handler) logging.getLogger().setLevel(logging.INFO) -
Configurer via un dictionnaire (dictConfig)
Pour les applis sérieuses, on évite la config impérative au profit d'un dictionnaire (souvent chargé depuis un fichier YAML). C'est plus lisible et plus maintenable.
pythonimport logging.config logging.config.dictConfig({ "version": 1, "formatters": { "default": {"format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s"}, }, "handlers": { "console": {"class": "logging.StreamHandler", "formatter": "default"}, "file": { "class": "logging.handlers.RotatingFileHandler", "filename": "app.log", "maxBytes": 5_000_000, "backupCount": 3, "formatter": "default", }, }, "root": {"level": "INFO", "handlers": ["console", "file"]}, }) -
Logger une exception avec sa trace
logger.exception()n'est utile que dans un blocexcept: il ajoute automatiquement la stack trace au message. Ne pas le confondre aveclogger.error()qui ne l'inclut pas.pythontry: risky_call() except Exception: logger.exception("appel risqué a échoué") -
Désactiver les logs d'une lib trop bavarde
Beaucoup de libs externes (urllib3, asyncio, sqlalchemy…) loguent en DEBUG, ce qui pollue tes logs. On les remonte à WARNING module par module :
pythonlogging.getLogger("urllib3").setLevel(logging.WARNING) logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING)
Si tu appelles directement logging.info("...") au lieu
de passer par logger.info("...") sur ton propre logger,
tu utilises le root logger. Ça marche, mais tu perds
l'info du module dans %(name)s, et tu rends impossible
de filtrer la verbosité par module. Toujours passer par
getLogger(__name__).
Aide-mémoire
import logging
logging.basicConfig(level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
logger = logging.getLogger(__name__)
logger.debug("détails techniques")
logger.info("étape normale")
logger.warning("anomalie sans gravité")
logger.error("opération échouée")
logger.critical("appli HS")
logger.exception("dans un except") # + traceback
# Variables disponibles dans le format :
"%(asctime)s" # horodatage
"%(levelname)s" # INFO, WARNING, ...
"%(name)s" # nom du logger (module)
"%(message)s" # le texte du log
"%(filename)s" # fichier source
"%(lineno)d" # numéro de ligne
"%(funcName)s" # nom de la fonction
logging.StreamHandler() # console
logging.FileHandler("app.log") # fichier simple
logging.handlers.RotatingFileHandler(...) # rotation par taille
logging.handlers.TimedRotatingFileHandler(...) # rotation par date
logging et le reste de l'écosystème
- Loguru — la lib externe moderne, plus simple à configurer. Elle peut remplacer ou intercepter les appels stdlib pour qu'ils passent par Loguru.
-
FastAPI — utilise stdlib
sous le capot (via uvicorn). On configure les loggers
uvicorn,uvicorn.access,fastapipour piloter les logs HTTP. - Celery — chaque worker Celery a son propre logger configurable, basé sur stdlib. Le format par défaut inclut l'ID de la tâche.
-
Loki — pour centraliser les
logs en prod, on les écrit en JSON puis on les expédie vers Loki via
un agent (Promtail, Fluent Bit). Pour ça, le format
%(message)sne suffit plus — on switch vers du JSON structuré (souvent avec une lib annexe).
Pour aller plus loin
- Doc officielle : docs.python.org/logging
- Tutoriel pas à pas : Logging HOWTO
- Cookbook officiel (cas avancés) : Logging Cookbook
- structlog — alternative pour logs structurés JSON : structlog.org