À 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.
Pourquoi pas juste 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.routes est enfant de app.api, lui-même enfant de app, lui-même enfant du root logger.
  • Handler — la destination. StreamHandler écrit sur la console, FileHandler dans un fichier, RotatingFileHandler dans 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)s donne 2026-05-11 14:32:01 [INFO] app.api: serveur démarré.
Les 5 niveaux standards

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

python
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 :

text
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

  1. 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")
  2. Configurer une seule fois, au point d'entrée

    La configuration se fait une seule fois, en haut de ton main.py ou dans un module dédié. Les autres fichiers se contentent de récupérer leur logger avec getLogger(__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",
    )
  3. Logger dans un fichier qui tourne

    RotatingFileHandler est l'outil standard : un fichier qui se renouvelle quand il atteint une taille donnée, avec un historique limité.

    python
    import 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)
  4. 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.

    python
    import 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"]},
    })
  5. Logger une exception avec sa trace

    logger.exception() n'est utile que dans un bloc except : il ajoute automatiquement la stack trace au message. Ne pas le confondre avec logger.error() qui ne l'inclut pas.

    python
    try:
        risky_call()
    except Exception:
        logger.exception("appel risqué a échoué")
  6. 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 :

    python
    logging.getLogger("urllib3").setLevel(logging.WARNING)
    logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING)
Le piège classique du root logger

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

python (basique)
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
python (formatters utiles)
# 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
python (handlers courants)
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, fastapi pour 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)s ne suffit plus — on switch vers du JSON structuré (souvent avec une lib annexe).

Pour aller plus loin