Du möchtest Logging-Ausgaben in Python sauber steuern?
- Du möchtest Logs in der Konsole abhängig von
-voder-vvausgeben? - 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:
- Logging wird einmal zentral im Programmeinstieg konfiguriert.
- Jedes Modul verwendet seinen eigenen Logger mit
logging.getLogger(__name__). - Die Ausgabeziele werden über Handler gesteuert.
- Das Aussehen der Ausgabe wird über Formatter gesteuert.
- Die Sichtbarkeit je nach
-voder-vvwird über das Log-Level des Console-Handlers gesteuert.
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
projekt/
├── main.py
├── logging_setup.py
└── my_module.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,
)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:
DEBUGkommt grundsätzlich durch den Root-Logger.- Der Console-Handler verwirft
DEBUG, weil er erst abWARNINGausgibt. - Der File-Handler schreibt
DEBUGin die Datei.
So kann die Datei ausführlich sein, während die Konsole ruhig bleibt.
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()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.
python main.pyKonsole:
⚠️ __main__: Beispiel-Warnung aus main
⚠️ my_module: Warnung aus my_module
❌ my_module: Fehler aus my_module
DEBUG und INFO erscheinen nicht.
python main.py -vKonsole:
ℹ️ __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.
python main.py -vvKonsole:
🔍 __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.
python main.py --log-file app.logKonsole:
⚠️ __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.
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")__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.
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("...")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")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.
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.
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
-voder-vv - Log-Datei optional
- Log-Datei immer ausführlich
- Konsole lesbar mit Symbolen
- Module sauber getrennt
- Konfiguration nur an einer Stelle