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().
One idea, one try. At your own pace.
LEARN BY DOINGPut your intuition to work3 steps
48 · Logging: levels, formats, basicConfig
Use the lesson’s examples and explanation to check your idea.
Personal reflection. Nothing is sent to an AI service.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.