Logging
Advanced · 18 min read · ▶ live playground · ✦ checkpoint
Your program can be perfectly correct at noon and mysterious by dinner. Logging gives you a trail: what happened, where it happened, and how serious it was. Instead of scattering temporary prints through the code, you leave useful runtime evidence on purpose.
Why logging beats print
Logging is recording events from a running program with extra context, like severity, how serious the event is, and where the message came from. Think of logging as a project diary: each entry says what happened, which part of the program wrote it, and whether it is routine news or a problem.
print() is still useful. You used it while debugging in Debugging Like a Pro, and you will keep using it for program output. The trouble starts when debug prints mix with real output:
- A user sees
DEBUG paid: noin the middle of a receipt. - You forget to remove a temporary print before sharing the code.
- You want noisy details while developing, but quiet output in normal runs.
- You want warnings saved to a file, not only flashed on screen.
The logging module is Python's standard-library tool for this job.
import logging
import sys
logging.basicConfig(
level=logging.INFO,
format="%(levelname)s:%(name)s:%(message)s",
stream=sys.stdout,
force=True,
)
logger = logging.getLogger("checkout")
player = "Noor"
apples = 2
logger.info(f"{player} opened checkout")
logger.warning(f"Only {apples} apples left")INFO:checkout:Noor opened checkoutWARNING:checkout:Only 2 apples left
stream=sys.stdout sends log lines to the normal printed-output channel so you can see them here. force=True resets any earlier logging setup in the current Python process, which keeps small runnable scripts and entry points in control of their own output. The key difference is not the destination. The key difference is structure: each line carries a level, a name, and a message.
Levels say how serious the event is
A log level is a label for how serious or detailed a log message is. Levels are a volume knob for your diary: during development you may want every scratch note; in normal runs you may only want warnings and failures.
The common levels, from quiet detail to loud emergency:
DEBUG— tiny details useful while investigating.INFO— normal milestones, like "loaded 12 scores."WARNING— something odd happened, but the program can continue.ERROR— an operation failed.CRITICAL— the program may not be able to keep running.
When you set the logging level to WARNING, Python hides DEBUG and INFO messages.
import logging
import sys
logging.basicConfig(
level=logging.WARNING,
format="%(levelname)s:%(name)s:%(message)s",
stream=sys.stdout,
force=True,
)
logger = logging.getLogger("game.round")
logger.debug("Mina entered the bonus room")
logger.info("Round started for Mina")
logger.warning("Mina has 1 life left")
logger.error("Mina score file is missing")WARNING:game.round:Mina has 1 life leftERROR:game.round:Mina score file is missing
That filtering is the first big win over print(). You can leave the DEBUG call in the code and decide from configuration whether it appears.
Try it - turn the diary volume
Run this once. Then change level=logging.INFO to level=logging.DEBUG and run it again. You are not editing the reservation logic; you are changing how much evidence the program shows.
Loggers give messages a source
A logger is a named object you send log messages to. The name is the diary label. checkout, game.round, and snack.counter tell you which part of the program spoke.
In a real project, you usually create one logger near the top of each module:
import logging
import sys
def configure_logging(debug=False):
level = logging.DEBUG if debug else logging.INFO
logging.basicConfig(
level=level,
format="%(levelname)s:%(name)s:%(message)s",
stream=sys.stdout,
force=True,
)
logger = logging.getLogger(__name__)
def award_badge(player, score):
logger.debug(f"Checking badge for {player} with score {score}")
if score >= 90:
logger.info(f"Awarded badge to {player}")
return "badge earned"
logger.warning(f"No badge for {player}: score {score}")
return "keep practicing"
configure_logging(debug=False)
print(award_badge("Ravi", 93))
print(award_badge("Mina", 72))INFO:__main__:Awarded badge to Ravibadge earnedWARNING:__main__:No badge for Mina: score 72keep practicing
__name__ is the current module's name. In a single script, it prints as __main__. In a multi-file project, names like scores.badges help you find the file that wrote the message.
Notice where the setup lives: configure_logging() runs near program startup. The function award_badge() uses the module logger and writes events, but it does not decide the global logging style. That split keeps setup choices out of the badge rules.
Handlers choose the destination
A handler is a logging object that sends log records to one destination. A log record is one structured logging event: level, logger name, message, and other details. One logger can have several handlers, like one diary entry copied to the console and to a file.
A formatter is a logging object that turns a log record into text. It is the page layout for each diary entry.
This example sends normal messages to the console and all details to a file, then reads the file back in the same run. Two cleanup lines keep the example from duplicating records: logger.handlers.clear() removes old destinations from this logger, and logger.propagate = False stops records from also traveling to parent loggers. The root logger is logging's top-level fallback logger, so blocking propagation keeps this custom setup isolated.
import logging
import sys
logger = logging.getLogger("library.checkout")
logger.setLevel(logging.DEBUG)
logger.handlers.clear()
logger.propagate = False
console = logging.StreamHandler(sys.stdout)
console.setLevel(logging.INFO)
console.setFormatter(logging.Formatter("%(levelname)s:%(message)s"))
file_handler = logging.FileHandler("library.log", mode="w", encoding="utf-8")
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(logging.Formatter("%(name)s|%(levelname)s|%(message)s"))
logger.addHandler(console)
logger.addHandler(file_handler)
logger.debug("Mina scanned card 4172")
logger.info("Mina borrowed Python Basics")
logger.warning("Mina has 1 renewal left")
print("--- file log ---")
with open("library.log", encoding="utf-8") as log_file:
for line in log_file:
print(line.rstrip())INFO:Mina borrowed Python BasicsWARNING:Mina has 1 renewal left--- file log ---library.checkout|DEBUG|Mina scanned card 4172library.checkout|INFO|Mina borrowed Python Basicslibrary.checkout|WARNING|Mina has 1 renewal left
The console stayed readable. The file kept the lower-level DEBUG evidence. That is the second big win over print(): one event can go to different places with different detail.
Configure once for the project
Logging configuration is the startup code that decides the level, format, and destinations for the whole program. Keep it in one place. Do not call basicConfig() inside every function.
A practical first setup:
- In each module, create
logger = logging.getLogger(__name__). - In your program entry point, call one
configure_logging()function. - Use
DEBUGfor investigation details,INFOfor normal milestones,WARNINGfor recoverable surprises, andERRORfor failed operations. - Start with console logs. Add a file handler when you need a saved trail.
- Keep user-facing output as
print()or return values. Keep runtime evidence as logs.
You now have a clean trail when the program runs. Next, you will learn how to measure performance with the same discipline: gather evidence first, then change the code.
Checkpoint
Answer all three to mark this lesson complete