skip to content

Why use the logging module instead of print() in a Python application?

level: juniorimportance: must knowfreq 60%

answer

  1. Diagnostics are not program output
  2. Every message carries a severity
  3. Configured once, not at each call site
  4. print() writes unconditionally to stdout
  5. logger.exception adds the traceback

basics

~20 s

Every logging call carries a severity level, a logger name and a timestamp, and where it goes is decided once at startup rather than at the call site. print() writes an unconditional bare line to stdout with none of that.

solid answer

~40 s

`print()` writes one unconditional line of text to stdout: no severity, no source name, no timestamp, and no way to turn it down without editing the call. A `logging` call records a severity (`logging.DEBUG` through `logging.CRITICAL`), the logger's dotted name and the time, and the destination and verbosity are decided once at process startup - `logging.basicConfig()` in a script, `logging.config.dictConfig()` in an application - so you can raise a subsystem to DEBUG in production without touching code. Diagnostics also belong on stderr, not mixed into a program's stdout output. Inside an `except` block, `logger.exception("...")` records the full traceback, which `print(exc)` throws away. The one legitimate use of `print()` is a CLI program's actual output for the user, which is data, not diagnostics.

code

python · 10 lines
python
import logging

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s %(message)s")
logger = logging.getLogger("ingest.parser")

try:
    int("not-a-number")
except ValueError:
    print("bad row")
    logger.exception("bad row")

go deeper

for a junior

Be ready to name what a logging call adds over print(): a severity level, the logger's name, a timestamp, and a destination chosen at startup. Know that logger.exception() inside an except block records the traceback.

for a middle

Explain the mechanics: DEBUG through CRITICAL as numeric constants, the root logger's WARNING default that makes unconfigured INFO calls disappear, stderr versus stdout, and one basicConfig call at the entry point rather than per module.

for a senior

Show the operational judgement: libraries log but never configure, verbosity is changed without a redeploy, tracebacks are captured with exc_info, and credentials and personal data never enter a log line no matter the level.

for a principal

Own the policy: what the team logs at each level, which fields are forbidden, who can raise verbosity in production and how, and the cost side - log volume is a bill and a retention risk, so noisy INFO logging is a design decision, not a free one.

### The distinction that matters A program produces two different streams of text. One is its **output**: what the user asked for, the rows a pipeline emits, the answer a CLI prints. The other is **diagnostics**: what the program is doing and how well it is going. `print()` is a fine tool for the first and a poor one for the second, and the entire `logging` module exists to serve the second. ### What a logging call carries that a print does not When you call `logger.info("row accepted")`, the module builds a `logging.LogRecord` carrying the message, a numeric severity, the logger's dotted name, the timestamp, the module, the line number, the process and thread ids, and (on request) exception information. A `print()` call carries a string. The severity is the practical payoff. The standard constants are `logging.DEBUG` (10), `logging.INFO` (20), `logging.WARNING` (30), `logging.ERROR` (40) and `logging.CRITICAL` (50), with `logging.NOTSET` (0) meaning "inherit". Because every message is tagged, a running system can be told to show INFO and above today and DEBUG for one noisy subsystem tomorrow, without editing a single call site. With `print()` the only volume control is a code change and a redeploy. The logger name is the second payoff. `logging.getLogger(__name__)` at the top of each module gives every message a stable dotted origin, so a log line says which part of the codebase spoke, and configuration can address that part by name. ### Where the output goes `print()` goes to `sys.stdout` unless you redirect it. Diagnostics on stdout are actively harmful when the program's real output is also on stdout - a data pipeline whose stdout is consumed by the next stage gets corrupted by a debugging line. Logging's default console handler writes to `sys.stderr`, which is the conventional channel for diagnostics and is line-buffered rather than block-buffered when attached to a terminal. Destination is also configurable in one place. A script calls `logging.basicConfig(level=logging.INFO)` once at startup; an application applies a whole configuration with `logging.config.dictConfig(...)` at its entry point. Libraries configure nothing at all - they only call `logging.getLogger(__name__)` and leave the decision to the application that imports them, which is exactly what a library sprinkled with `print()` cannot do. ### The beginner surprise With no configuration at all, `logging.getLogger("x").info("hi")` prints nothing, while `.warning("hi")` appears on stderr. That is not a bug: the root logger defaults to WARNING, and since Python 3.2 a fallback handler, `logging.lastResort`, emits WARNING and above to stderr when no handler is configured. People conclude "logging is broken" and go back to `print()`. The fix is one line of `basicConfig` at startup, not abandoning the module. ### Exceptions In an `except` block, `print(exc)` gives you `invalid literal for int()` and nothing else - no traceback, no idea which line raised. `logger.exception("parse failed")` logs at ERROR **and** attaches the active exception's traceback; `logger.error("parse failed", exc_info=True)` is the same thing at an explicit level. That single habit is often the difference between a five-minute diagnosis and an afternoon of guessing. ### What logging does not excuse Logs persist, get copied into search systems and are readable by far more people than the code is. So the rules of the tap apply to what you put into it: never log passwords, tokens, session cookies, authorization headers, full card numbers or raw request bodies, and prefer identifiers (a user id, a request id) to payloads. Logging an object whose `__repr__` includes a credential leaks it just as effectively as printing it - the module gives you routing, not discretion. ### When print() is still right A CLI's user-facing result, a REPL experiment, a one-off script you will delete, and the output of a tool designed to be piped. "It is faster" is not a reason: at INFO and above the difference is irrelevant, and a filtered-out `logger.debug()` costs less than a `print()` you left in.

  • Inside an except block, how do you record the traceback and not just the exception's message?
    Call `logger.exception("parse failed")`, which logs at ERROR and attaches the exception currently being handled. `logger.error("parse failed", exc_info=True)` is identical apart from letting you choose the level, and outside an active handler you can pass the exception object itself as `exc_info=exc`. `print(exc)` or `logger.error(str(exc))` records only the message, losing the stack that tells you where it came from.
  • What should never end up in a log line, even at DEBUG?
    Credentials and personal data: passwords, API tokens, session cookies, `Authorization` headers, full card numbers, and raw request or response bodies that may contain any of those. Logs are copied, retained and read by many more people than the source is. Log identifiers instead - a user id, a request id, a record key - and remember that logging an object whose `__repr__` embeds a secret leaks it exactly as a `print()` would.
  • Should a library call logging.basicConfig()?
    No. A library obtains `logging.getLogger(__name__)` and logs; the application that imports it decides levels and destinations. A `basicConfig()` call at import time in a library installs a root handler behind the application's back and, because `basicConfig` does nothing when the root logger already has handlers, whichever side runs first silently wins. If a library wants to avoid a no-handler warning it can attach `logging.NullHandler()` to its own top-level logger.

print() is shouting across the room; logging is filing a report with a severity stamp and a sender, which someone downstream can file, forward or ignore.

saying these in an interview costs you the question

  • print() is fine, we just grep stdout
  • Logging is slow, so print in hot paths
  • print(exc) records the traceback too
  • Log the whole request body for debugging
  • Libraries should call basicConfig on import
  • Diagnostics belong on stdout with the results

context