Definition
A program running with nobody watching the screen ends up sooner or later running into something unexpected. The knowing comes after the fact: without any written trace along the way, all that is left is the final result, which never tells the story that led up to it.
logging is the standard library module that writes this story as the program moves forward. Every message leaves with a timestamp, a severity level and the name of the place it came from, then lands wherever the configuration sends it: terminal, file, collection service. The code says what happened, the configuration decides who hears about it.
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
logger.info("Import started, %s rows to process", 1200)
logger.warning("Row 47 skipped, unreadable date")The __name__ handed to getLogger is not a style detail: it sticks the exact origin onto the message, and later lets one chatty file be silenced without silencing the others.
print or logging
The first instinct while writing code is to drop in a print to watch a value go by, which works as long as somebody is watching the terminal. Once the program ships, those print calls become permanent noise, or messages someone would have wanted to keep.
logging repeats the same gesture, but adds what a plain print cannot do alone: an automatic timestamp, a level that can be filtered after the fact, a destination that can be changed without touching the code. Here are the two side by side:
| Need | With print | With logging |
|---|---|---|
| Seeing a value while writing the code | Perfect | Needlessly heavy |
| Knowing what time it happened | Write it yourself | Automatic timestamp |
| Cutting the noise in production | Delete the lines | Raise a threshold |
| Writing to a file | Redirect the terminal | Add a handler |
| Keeping the stack of a crash | By hand | A single method |
There are cases where logging brings nothing, a thirty-line script for instance. More importantly: a program that prints its result must use print, never logging, the first answering the person waiting, the second narrating behind the scenes what the program is doing.
The five levels
Writing a message at every step sounds like a good idea, until the terminal overflows and the one that matters drowns in the detail. logging answers by attaching a level to each message, compared against a configured threshold: only the messages that reach it get shown.
Here are the five levels, from the most common to the most severe:
| Level | What it reports |
|---|---|
DEBUG | The detail worth having while hunting a bug, invisible the rest of the time |
INFO | A normal step: the job starts, the file is written |
WARNING | Something odd that the program managed to work around |
ERROR | An operation failed, the program carries on |
CRITICAL | The program cannot carry on |
The default threshold is WARNING. A program that has just been configured therefore shows neither its DEBUG nor its INFO messages, which catches nearly everyone off guard the first time.
The first-day trap
"I call logger.info and nothing shows up." That is this same threshold surprise: the message is not lost, it is filtered. The fix takes one call, made once, at the entry point of the program, before any other message:
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s : %(message)s",
)That call hides a second surprise, sneakier still: basicConfig does nothing once logging is already configured, and a first message emitted before the call configures it by default, without warning. A call made too late is therefore ignored in silence.
Anyone writing a library meant for others follows the opposite rule: never call basicConfig inside it, or you force your format onto the program installing it.
Recording an error without losing the traceback
Inside an except block, the reflex is to copy the exception message and move on. That throws away the most useful part: the traceback, which says on which line the problem was born. Without it, that path has to be pieced back together from memory, or the bug reproduced just to see it again.
The exception method attaches it on its own, and belongs only in a rescue block:
try:
unit = invoice["amount"] / invoice["quantity"]
except ZeroDivisionError:
logger.exception("Zero quantity on invoice %s", invoice["id"])
unit = 0Note the comma, where an f-string would have been tempting. The message stays a constant template, which lets a collection tool group a thousand occurrences of one incident instead of showing a thousand distinct lines. The performance gain sometimes cited stays negligible outside very hot loops.
Frequently asked questions
One logger per file, or a single one for the whole program?
One per file, always with the same line at the top: logger = logging.getLogger(__name__). Loggers form a hierarchy built on the dots in the name, so shop.payment inherits the settings of shop, which allows a single module to be switched to DEBUG without drowning the rest.
Should logs be written to a file?
It mostly depends on where the program runs. On a classic server, yes, with a handler that rolls the file over past a certain size, or it becomes unreadable. In a container, no: standard output is enough, the host collects and archives it, and that is usually how a FastAPI or Django application is deployed.
What should never be written to a log?
A password, a token, a card number, the full contents of a form. A log is kept for months and ends up read by people with no access to the database. Record the identifier of the user involved rather than their data, and avoid writing a whole json response without knowing exactly what it holds.