Skip to content

Python logging — Structured Application Logs

The logginglogging module is the standard way to record what your program is doing. Unlike printprint, it has severity levels, can route messages to files or the network, includes timestamps and source info, and can be turned up or down without touching your code.

quickstart.py
import logging
 
logging.basicConfig(level=logging.INFO)
logging.info("App started")
logging.warning("Low disk space")
logging.error("Connection failed")
# INFO:root:App started
# WARNING:root:Low disk space
# ERROR:root:Connection failed
quickstart.py
import logging
 
logging.basicConfig(level=logging.INFO)
logging.info("App started")
logging.warning("Low disk space")
logging.error("Connection failed")
# INFO:root:App started
# WARNING:root:Low disk space
# ERROR:root:Connection failed

Why not just use print?

printprintlogginglogging
One firehose to stdout.Severity levels you can filter.
No timestamps or context.Timestamps, module, line number, etc.
Edit code to silence it.Change one config line.
Goes one place.Files, console, syslog, email — many handlers.

Log levels

Each message has a level. The logger only emits messages at or above its configured level.

LevelValueUse for
DEBUGDEBUG10Detailed diagnostic info.
INFOINFO20Normal events (“server started”).
WARNINGWARNING30Something unexpected, but still working (the default).
ERRORERROR40A failure in some operation.
CRITICALCRITICAL50A serious failure; the program may stop.
levels.py
import logging
 
logging.basicConfig(level=logging.DEBUG)
logging.debug("variable x = 42")     # shown only because level is DEBUG
logging.info("processing record")
logging.warning("retrying")
logging.error("gave up")
print(logging.getLevelName(20))      # INFO
levels.py
import logging
 
logging.basicConfig(level=logging.DEBUG)
logging.debug("variable x = 42")     # shown only because level is DEBUG
logging.info("processing record")
logging.warning("retrying")
logging.error("gave up")
print(logging.getLevelName(20))      # INFO

The default level is WARNINGWARNING, so debugdebug and infoinfo messages are hidden until you lower the level with basicConfig(level=...)basicConfig(level=...).

basicConfig — quick setup

basicConfigbasicConfig configures the root logger in one call. Set it once, early, before logging anything.

basic_config.py
import logging
 
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
    datefmt="%H:%M:%S",
)
logging.info("ready")
# 14:30:01 [INFO] root: ready
 
# Write to a file instead of the console
logging.basicConfig(filename="app.log", level=logging.INFO)
basic_config.py
import logging
 
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
    datefmt="%H:%M:%S",
)
logging.info("ready")
# 14:30:01 [INFO] root: ready
 
# Write to a file instead of the console
logging.basicConfig(filename="app.log", level=logging.INFO)

Common format placeholders

PlaceholderInserts
%(asctime)s%(asctime)sHuman-readable timestamp.
%(levelname)s%(levelname)sThe level name (INFO, ERROR, …).
%(name)s%(name)sThe logger’s name.
%(message)s%(message)sThe log message.
%(filename)s%(filename)s / %(lineno)d%(lineno)dSource file / line number.

Named loggers — one per module

Instead of the root logger, create a logger per module with getLogger(__name__)getLogger(__name__). This tags every message with where it came from and lets you tune modules independently.

named_loggers.py
import logging
 
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
 
logger.info("module-specific message")
# INFO:__main__:module-specific message
named_loggers.py
import logging
 
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
 
logger.info("module-specific message")
# INFO:__main__:module-specific message

Handlers and formatters

For real apps, attach handlers (where logs go) and formatters (how they look). You can have several at once — e.g. INFO to console, ERROR to a file.

handlers.py
import logging
 
logger = logging.getLogger("myapp")
logger.setLevel(logging.DEBUG)
 
# Console handler at INFO
console = logging.StreamHandler()
console.setLevel(logging.INFO)
console.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
 
# File handler at DEBUG
file = logging.FileHandler("debug.log")
file.setLevel(logging.DEBUG)
 
logger.addHandler(console)
logger.addHandler(file)
 
logger.debug("only in the file")
logger.info("console + file")
handlers.py
import logging
 
logger = logging.getLogger("myapp")
logger.setLevel(logging.DEBUG)
 
# Console handler at INFO
console = logging.StreamHandler()
console.setLevel(logging.INFO)
console.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
 
# File handler at DEBUG
file = logging.FileHandler("debug.log")
file.setLevel(logging.DEBUG)
 
logger.addHandler(console)
logger.addHandler(file)
 
logger.debug("only in the file")
logger.info("console + file")

Logging exceptions

Inside an exceptexcept block, logger.exception(...)logger.exception(...) logs the message plus the full traceback.

exceptions.py
import logging
 
logging.basicConfig(level=logging.ERROR)
try:
    1 / 0
except ZeroDivisionError:
    logging.exception("Math went wrong")
# ERROR:root:Math went wrong
# Traceback (most recent call last): ...
exceptions.py
import logging
 
logging.basicConfig(level=logging.ERROR)
try:
    1 / 0
except ZeroDivisionError:
    logging.exception("Math went wrong")
# ERROR:root:Math went wrong
# Traceback (most recent call last): ...

Common pitfalls

  • Default level hides INFO/DEBUG — call basicConfig(level=logging.DEBUG)basicConfig(level=logging.DEBUG) to see them.
  • basicConfigbasicConfig only works once — the first call wins; later calls are ignored unless force=Trueforce=True.
  • Use %s%s lazy formatting: logger.info("user %s", name)logger.info("user %s", name), not f-strings, so the string is only built if the message is actually emitted.
  • Don’t log secrets — passwords, tokens, and personal data should never hit the logs.

Practice Exercises

Exercise 1 – Level names and values

Exercise 2 – Configure and log info

Exercise 3 – Create a named logger

Summary

  • logginglogging replaces printprint with leveled, timestamped, routable messages.
  • Levels: DEBUG < INFO < WARNING < ERROR < CRITICALDEBUG < INFO < WARNING < ERROR < CRITICAL; default is WARNINGWARNING.
  • Configure quickly with basicConfig(level=..., format=...)basicConfig(level=..., format=...).
  • Use getLogger(__name__)getLogger(__name__) per module, and attach handlers/formatters for fine control.
  • Log tracebacks with logger.exceptionlogger.exception inside exceptexcept blocks.

If this helped you, consider buying me a coffee ☕

Buy me a coffee

Was this page helpful?

Let us know how we did