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.
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 failedimport 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 failedWhy not just use print?
printprint | logginglogging |
|---|---|
| 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.
| Level | Value | Use for |
|---|---|---|
DEBUGDEBUG | 10 | Detailed diagnostic info. |
INFOINFO | 20 | Normal events (“server started”). |
WARNINGWARNING | 30 | Something unexpected, but still working (the default). |
ERRORERROR | 40 | A failure in some operation. |
CRITICALCRITICAL | 50 | A serious failure; the program may stop. |
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)) # INFOimport 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)) # INFOThe default level is
WARNINGWARNING, sodebugdebugandinfoinfomessages are hidden until you lower the level withbasicConfig(level=...)basicConfig(level=...).
basicConfig — quick setup
basicConfigbasicConfig configures the root logger in one call. Set it once, early, before logging anything.
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)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
| Placeholder | Inserts |
|---|---|
%(asctime)s%(asctime)s | Human-readable timestamp. |
%(levelname)s%(levelname)s | The level name (INFO, ERROR, …). |
%(name)s%(name)s | The logger’s name. |
%(message)s%(message)s | The log message. |
%(filename)s%(filename)s / %(lineno)d%(lineno)d | Source 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.
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
logger.info("module-specific message")
# INFO:__main__:module-specific messageimport logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
logger.info("module-specific message")
# INFO:__main__:module-specific messageHandlers 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.
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")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.
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): ...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. basicConfigbasicConfigonly works once — the first call wins; later calls are ignored unlessforce=Trueforce=True.- Use
%s%slazy 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
loggingloggingreplacesprintprintwith leveled, timestamped, routable messages.- Levels:
DEBUG < INFO < WARNING < ERROR < CRITICALDEBUG < INFO < WARNING < ERROR < CRITICAL; default isWARNINGWARNING. - 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.exceptioninsideexceptexceptblocks.
If this helped you, consider buying me a coffee ☕
Buy me a coffeeWas this page helpful?
Let us know how we did
