48 · Logging: levels, formats, basicConfig
Replace scattered print() calls with structured, level-aware logging. Learn the five standard levels, configure the root logger with basicConfig, write named module loggers, and capture full tracebacks automatically with logger.exception().
Python's `logging` module gives every message a severity level (DEBUG → CRITICAL) and lets you dial the verbosity threshold without touching a single line of application code. You set `basicConfig(level=logging.WARNING)` in production and your DEBUG/INFO messages disappear — zero code changes, zero extra output. `print()` has no equivalent: it's a one-way trip to stdout with no concept of severity, no timestamps, and no mechanism to silence or redirect it selectively.
Without this:
Without structured logging you litter your codebase with `print()` calls that you must manually delete or comment out before shipping. There is no way to silence verbose debug output in production without a search-and-replace across dozens of files, and no automatic timestamping or traceback capture.
Why logging beats print
When you write print("epoch 1 loss=0.42"), that string goes to stdout and stays there — you cannot silence it without deleting the line, you cannot attach a timestamp, and you cannot route it to a file without redirecting the entire process. The logging module solves all three problems:
- Levels — every record has a severity:
DEBUG,INFO,WARNING,ERROR, orCRITICAL. You configure a threshold once; anything below it is silently discarded. - Timestamps and metadata — the format string can include
%(asctime)s,%(name)s,%(levelname)s, and%(message)swithout any extra code. - Easy redirection — send the same log to stderr, a file, a socket, or a third-party service by adding a handler. Nothing in the application code changes.
- Library-friendly — a well-behaved library uses
logging.getLogger(__name__)and never callsbasicConfig. The application that uses the library decides what to do with those records.
The five levels (lowest → highest)
| Level | Value | Meaning |
|-------|-------|---------|
| DEBUG | 10 | Detailed diagnostic — variable values, loop counts |
| INFO | 20 | Confirmation that things are working as expected |
| WARNING | 30 | Something unexpected — pipeline continues but you should know |
| ERROR | 40 | A serious problem — an operation failed |
| CRITICAL | 50 | A severe error — the program may not be able to continue |
Setting basicConfig(level=logging.INFO) means INFO, WARNING, ERROR, and CRITICAL are emitted; DEBUG is silenced. Raising it to WARNING silences both DEBUG and INFO.
Pyodide note
Pyodide's Python runtime persists between "Run" clicks in the same browser session — meaning the root logger accumulates handlers each time a cell runs. To keep each cell's output predictable, the runnable cells below start with importlib.reload(logging) which resets logging to a clean slate.
Python (in browser)
With `level=logging.DEBUG` all five levels are emitted. Change it to `logging.WARNING` and only the last three lines appear — no application code needs to change.
Python runs entirely in your browser via Pyodide (~6 MB on first Run, cached after).
Python (in browser)
The `%(asctime)s` field uses `datefmt` for its clock format. `%(levelname)s` is left-padded to 8 chars by default — useful for visual alignment in terminal output. Notice that `WARNING` and above appear even though `level=INFO` was set; `DEBUG` would be filtered.
Python runs entirely in your browser via Pyodide (~6 MB on first Run, cached after).
Python (in browser)
`"training.evaluation"` is a *child* of `"training"` — both the logger name and the dot-separated hierarchy are visible in the `%(name)s` field. Child loggers propagate records to their parent by default, so a single handler on the root logger catches everything.
Python runs entirely in your browser via Pyodide (~6 MB on first Run, cached after).
Python (in browser)
`logger.exception(msg)` is shorthand for `logger.error(msg, exc_info=True)`. It logs at ERROR level and appends the current exception's full traceback — without needing `import traceback` or any extra code. Note the `%`-style placeholders in `logger.info("batch %d", batch_id)`: logging traditionally uses lazy `%`-formatting so the string is only built if the record will actually be emitted. f-strings work too, but they're always evaluated even when the level is filtered out.
Python runs entirely in your browser via Pyodide (~6 MB on first Run, cached after).
You call `logging.basicConfig(level=logging.WARNING)`. Which of the following calls will actually produce output?
- `logging` is stdlib — `import logging`, no installation needed. Always prefer it over `print()` for anything that runs longer than a one-off script.
- The five levels from lowest to highest: `DEBUG (10)` → `INFO (20)` → `WARNING (30)` → `ERROR (40)` → `CRITICAL (50)`. The threshold set in `basicConfig(level=...)` filters everything below it.
- `logging.basicConfig(level=..., format='...', datefmt='...')` configures the root logger. Key format fields: `%(asctime)s`, `%(name)s`, `%(levelname)s`, `%(message)s`.
- Every module should declare `logger = logging.getLogger(__name__)` at the top. Named loggers form a dot-separated hierarchy — `"training.evaluation"` is a child of `"training"`.
- `logger.exception(msg)` inside an `except` block logs at ERROR and automatically appends the full traceback — no `import traceback` needed.
Training loops log loss and metrics per epoch via `logger.info("epoch=%d loss=%.4f", epoch, loss)`. Data pipeline warnings — skipped bad rows, NaN counts, class imbalance alerts — go through `logger.warning`. `logger.exception` is how you capture failed batches without losing the traceback: wrap the forward pass in `try/except` and call `logger.exception("batch %d failed", batch_id)`. Production training scripts typically set `level=INFO` and route to a rotating file handler so long runs don't lose their history.
If you remove it: Without structured logging you fall back to `print()` statements that you must manually delete before shipping — or leave in, bloating stdout. There is no way to silence verbose debug output in production without modifying code, no automatic timestamps, and no traceback capture without manually calling `traceback.print_exc()` in every except block.