Skip to content

Instantly share code, notes, and snippets.

@TorstenC
Created May 24, 2026 00:00
Show Gist options
  • Select an option

  • Save TorstenC/b9524276c12ca6a37dea18fe5c067e7b to your computer and use it in GitHub Desktop.

Select an option

Save TorstenC/b9524276c12ca6a37dea18fe5c067e7b to your computer and use it in GitHub Desktop.

Python Logging mit -v, -vv, Konsole, Log-Datei und unterschiedlichen Formaten

Du möchtest Logging-Ausgaben in Python sauber steuern?

  • Du möchtest Logs in der Konsole abhängig von -v oder -vv ausgeben?
  • Du möchtest optional zusätzlich in eine Log-Datei schreiben?
  • Du möchtest auf der Konsole ein anderes Format verwenden als in der Log-Datei?
  • Du möchtest in der Konsole Symbole wie ℹ️, ⚠️ oder je nach Log-Level anzeigen?
  • Du möchtest Logging aus mehreren selbst geschriebenen Modulen zentral konfigurieren?

Dann ist das typische Muster:

  1. Logging wird einmal zentral im Programmeinstieg konfiguriert.
  2. Jedes Modul verwendet seinen eigenen Logger mit logging.getLogger(__name__).
  3. Die Ausgabeziele werden über Handler gesteuert.
  4. Das Aussehen der Ausgabe wird über Formatter gesteuert.
  5. Die Sichtbarkeit je nach -v oder -vv wird über das Log-Level des Console-Handlers gesteuert.

Grundidee

In Python erzeugt der Logger nur Log-Ereignisse.

logger.info("Programm gestartet")
logger.warning("Datei fehlt")
logger.error("Verarbeitung fehlgeschlagen")

Wohin diese Meldungen gehen und wie sie aussehen, entscheiden die Handler und Formatter.

Vereinfacht:

logger.info(...)
        ↓
LogRecord
        ↓
Handler, z. B. StreamHandler(sys.stderr)
        ↓
Formatter
        ↓
Ausgabe auf Konsole oder in Datei

Beispielstruktur

projekt/
├── main.py
├── logging_setup.py
└── my_module.py

logging_setup.py

import logging
import sys


class IconFormatter(logging.Formatter):
    """
    Formatter für Konsolenausgaben mit Symbolen je Log-Level.

    Der Formatter ergänzt jeden LogRecord vor der Ausgabe um das Feld
    `level_icon`. Dieses Feld kann dann im Format-String verwendet werden.
    """

    LEVEL_ICONS = {
        logging.DEBUG: "🔍",
        logging.INFO: "ℹ️",
        logging.WARNING: "⚠️",
        logging.ERROR: "❌",
        logging.CRITICAL: "💥",
    }

    def format(self, record: logging.LogRecord) -> str:
        record.level_icon = self.LEVEL_ICONS.get(record.levelno, "•")
        return super().format(record)


def configure_logging(verbosity: int, log_file: str | None = None) -> None:
    """
    Konfiguriert Logging zentral für das gesamte Programm.

    Konsole:
        - ohne -v: WARNING und höher
        - mit -v:   INFO und höher
        - mit -vv:  DEBUG und höher

    Log-Datei:
        - falls angegeben: immer DEBUG und höher

    Wichtig:
        Der Root-Logger wird auf DEBUG gesetzt, damit die einzelnen Handler
        selbst entscheiden können, was sie ausgeben.
    """

    if verbosity <= 0:
        console_level = logging.WARNING
    elif verbosity == 1:
        console_level = logging.INFO
    else:
        console_level = logging.DEBUG

    handlers: list[logging.Handler] = []

    # Ausgabe auf stderr, also die normale Fehler-/Log-Ausgabe der Konsole.
    console_handler = logging.StreamHandler(sys.stderr)
    console_handler.setLevel(console_level)

    console_handler.setFormatter(
        # Statt logging.Formatter(...) verwenden wir unseren eigenen Formatter.
        # IconFormatter ergänzt den LogRecord um `level_icon`.
        IconFormatter("%(level_icon)s %(name)s: %(message)s")
    )

    handlers.append(console_handler)

    if log_file:
        file_handler = logging.FileHandler(log_file, encoding="utf-8")
        file_handler.setLevel(logging.DEBUG)

        file_handler.setFormatter(
            logging.Formatter(
                "%(asctime)s %(levelname)s %(name)s: %(message)s"
            )
        )

        handlers.append(file_handler)

    logging.basicConfig(
        level=logging.DEBUG,
        handlers=handlers,
        force=True,
    )

Warum level=logging.DEBUG bei basicConfig()?

Das hier wirkt zunächst widersprüchlich:

logging.basicConfig(
    level=logging.DEBUG,
    handlers=handlers,
    force=True,
)

Der Grund ist:

Der Root-Logger soll zunächst alles ab DEBUG durchlassen.

Danach entscheiden die einzelnen Handler, was wirklich ausgegeben wird.

Beispiel:

Root-Logger:     DEBUG
Console-Handler: WARNING
File-Handler:    DEBUG

Dann gilt:

  • DEBUG kommt grundsätzlich durch den Root-Logger.
  • Der Console-Handler verwirft DEBUG, weil er erst ab WARNING ausgibt.
  • Der File-Handler schreibt DEBUG in die Datei.

So kann die Datei ausführlich sein, während die Konsole ruhig bleibt.


main.py

import argparse
import logging

from logging_setup import configure_logging
from my_module import do_something

logger = logging.getLogger(__name__)


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser()

    parser.add_argument(
        "-v",
        "--verbose",
        action="count",
        default=0,
        help="mehr Log-Ausgaben anzeigen, mehrfach verwendbar: -v, -vv",
    )

    parser.add_argument(
        "--log-file",
        help="Logs zusätzlich in eine Datei schreiben",
    )

    return parser.parse_args()


def main() -> None:
    args = parse_args()

    configure_logging(
        verbosity=args.verbose,
        log_file=args.log_file,
    )

    logger.debug("Debug-Ausgabe aus main")
    logger.info("Programm gestartet")
    logger.warning("Beispiel-Warnung aus main")

    do_something()

    logger.info("Programm beendet")


if __name__ == "__main__":
    main()

my_module.py

import logging

logger = logging.getLogger(__name__)


def do_something() -> None:
    logger.debug("Debug-Ausgabe aus my_module")
    logger.info("my_module arbeitet")
    logger.warning("Warnung aus my_module")
    logger.error("Fehler aus my_module")

Das Modul kennt weder args.verbose noch args.log_file.

Das ist Absicht.

Jedes Modul holt sich nur seinen eigenen Logger:

logger = logging.getLogger(__name__)

Die zentrale Konfiguration passiert nur einmal in main.py.


Beispielaufrufe

Ohne -v

python main.py

Konsole:

⚠️ __main__: Beispiel-Warnung aus main
⚠️ my_module: Warnung aus my_module
❌ my_module: Fehler aus my_module

DEBUG und INFO erscheinen nicht.


Mit -v

python main.py -v

Konsole:

ℹ️ __main__: Programm gestartet
⚠️ __main__: Beispiel-Warnung aus main
ℹ️ my_module: my_module arbeitet
⚠️ my_module: Warnung aus my_module
❌ my_module: Fehler aus my_module
ℹ️ __main__: Programm beendet

INFO, WARNING, ERROR und CRITICAL erscheinen.


Mit -vv

python main.py -vv

Konsole:

🔍 __main__: Debug-Ausgabe aus main
ℹ️ __main__: Programm gestartet
⚠️ __main__: Beispiel-Warnung aus main
🔍 my_module: Debug-Ausgabe aus my_module
ℹ️ my_module: my_module arbeitet
⚠️ my_module: Warnung aus my_module
❌ my_module: Fehler aus my_module
ℹ️ __main__: Programm beendet

Jetzt erscheinen auch DEBUG-Meldungen.


Mit Log-Datei

python main.py --log-file app.log

Konsole:

⚠️ __main__: Beispiel-Warnung aus main
⚠️ my_module: Warnung aus my_module
❌ my_module: Fehler aus my_module

Datei app.log:

2026-05-24 12:00:00,123 DEBUG __main__: Debug-Ausgabe aus main
2026-05-24 12:00:00,124 INFO __main__: Programm gestartet
2026-05-24 12:00:00,124 WARNING __main__: Beispiel-Warnung aus main
2026-05-24 12:00:00,125 DEBUG my_module: Debug-Ausgabe aus my_module
2026-05-24 12:00:00,125 INFO my_module: my_module arbeitet
2026-05-24 12:00:00,126 WARNING my_module: Warnung aus my_module
2026-05-24 12:00:00,126 ERROR my_module: Fehler aus my_module
2026-05-24 12:00:00,127 INFO __main__: Programm beendet

Die Konsole richtet sich nach -v.

Die Datei schreibt immer alles ab DEBUG.


Warum muss man args.verbose nicht an Module weitergeben?

Weil logging in Python einen gemeinsamen Logging-Zustand innerhalb des laufenden Python-Prozesses verwaltet.

Wenn in main.py einmal konfiguriert wird:

configure_logging(args.verbose, args.log_file)

dann betrifft diese Konfiguration auch Logger aus importierten Modulen.

Ein importiertes Modul braucht deshalb nur:

import logging

logger = logging.getLogger(__name__)

und kann dann ganz normal loggen:

logger.info("Hallo aus dem Modul")

Was bedeutet logging.getLogger(__name__)?

__name__ ist der Name des aktuellen Moduls.

In main.py ist das beim direkten Aufruf meistens:

__main__

In my_module.py ist es:

my_module

Dadurch sieht man in der Log-Ausgabe, aus welchem Modul die Meldung stammt:

ℹ️ my_module: my_module arbeitet

Bei Paketen kann das z. B. so aussehen:

meinpaket.untermodul

Das ist praktisch, weil man später sehr genau sieht, welcher Programmteil eine Meldung erzeugt hat.


Wichtig: In Modulen nicht basicConfig() aufrufen

In importierten Modulen sollte man das hier nicht tun:

logging.basicConfig(...)

Auch das sollte man dort nicht tun:

configure_logging(...)

Die Logging-Konfiguration gehört einmalig in den Programmeinstieg.

In Modulen genügt:

import logging

logger = logging.getLogger(__name__)

Danach verwendet man:

logger.debug("...")
logger.info("...")
logger.warning("...")
logger.error("...")
logger.critical("...")

logging.warn() nicht mehr verwenden

Früher sah man manchmal:

logging.warn("...")

Das sollte man heute nicht mehr verwenden.

Richtig ist:

logging.warning("...")

Dasselbe gilt für Logger:

logger.warning("Das ist eine Warnung")

Logging mit Variablen

Besser nicht so:

logger.info(f"Verarbeite Datei {path}")

Sondern so:

logger.info("Verarbeite Datei %s", path)

Der Vorteil:

Die Formatierung passiert erst dann, wenn die Meldung tatsächlich ausgegeben wird.

Das ist besonders bei DEBUG-Logs sinnvoll, die oft gar nicht sichtbar sind.


Exceptions loggen

Innerhalb eines except-Blocks ist logger.exception(...) praktisch:

try:
    1 / 0
except ZeroDivisionError:
    logger.exception("Berechnung fehlgeschlagen")

Das ist ungefähr wie logger.error(...), ergänzt aber automatisch den Stacktrace.

logger.exception(...) sollte man nur innerhalb eines except-Blocks verwenden.


Zusammenfassung

Das zentrale Muster lautet:

# main.py
configure_logging(args.verbose, args.log_file)
# jedes Modul
logger = logging.getLogger(__name__)

Die wichtigsten Rollen:

Logger     erzeugt Log-Meldungen
Handler    entscheidet, wohin geschrieben wird
Formatter  entscheidet, wie die Ausgabe aussieht
Level      entscheidet, ab welcher Wichtigkeit etwas ausgegeben wird

Für viele kleine und mittlere Kommandozeilenprogramme ist dieses Setup ein guter, übersichtlicher Standard:

  • Konsole abhängig von -v oder -vv
  • Log-Datei optional
  • Log-Datei immer ausführlich
  • Konsole lesbar mit Symbolen
  • Module sauber getrennt
  • Konfiguration nur an einer Stelle
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment