Python Logging

Replace print statements with the logging module: choose levels, name loggers, format output, record exceptions and protect private data.

Lesson 59 of 107Advanced12 min read

The logging module records what a program is doing, with a level for each message (DEBUG, INFO, WARNING, ERROR or CRITICAL) and configurable output: the console, a file, or a log collection service. Unlike print(), log messages can be filtered by level, carry timestamps and module names, and be switched on or off without editing code.

Python
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
logger = logging.getLogger(__name__)
logger.info("Import started")
logger.warning("Skipped shelf %s: missing title", "B7")

This lesson covers choosing levels, naming loggers, the %s argument style, logging exceptions with their tracebacks, and what must never appear in logs. The examples import a library's book catalogue.

Log levels

Each level signals how important a message is:

LevelNumberUse for
DEBUG10detailed diagnostics while developing
INFO20normal milestones: started, finished, counts
WARNING30something unexpected, but the program continues
ERROR40an operation failed
CRITICAL50the program can't continue

A logger drops messages below its configured level. The default is WARNING, which is why logging.info("...") prints nothing until you configure a lower level with logging.basicConfig(level=logging.INFO). In development you might run at DEBUG; in production, INFO or WARNING.

Choose levels honestly. If every progress message is logged as ERROR, real failures are buried and alerting systems fire constantly. A skipped record with a known reason is a WARNING; a database that can't be reached is an ERROR.

Logger names

Get a logger per module with logging.getLogger(__name__). The name follows the module path, such as catalog.importer, and loggers form a hierarchy by dots: catalog.importer is a child of catalog, which is a child of the root logger.

Python
logger = logging.getLogger(__name__)    # in catalog/importer.py: "catalog.importer"

Messages travel up the hierarchy to the handlers configured on ancestors, usually the root. That lets the application configure output once, at start-up, while each module just logs. It also allows targeted tuning: logging.getLogger("catalog.importer").setLevel(logging.DEBUG) shows detailed messages from one module only, and logging.getLogger("urllib3").setLevel(logging.WARNING) quietens a chatty library.

Libraries should never call basicConfig() or add handlers to the root logger; that's the application's decision. A library module just creates its logger and logs.

Deferred formatting

Pass values as arguments rather than building the string yourself:

Python
logger.debug("Processed %s records from %s", count, filename)   # preferred
logger.debug(f"Processed {count} records from {filename}")      # works, but always formats

With the argument style, the message is only formatted if a handler will actually output it, so DEBUG messages cost almost nothing in production. It also keeps the message template constant, which lets log analysis tools group identical messages that differ only in their values.

The format of each line is set by a formatter, through basicConfig(format=...) or on a handler. Common fields include %(asctime)s, %(levelname)s, %(name)s, %(message)s, %(funcName)s and %(lineno)d. Handlers decide where messages go: StreamHandler for the console, FileHandler for a file, RotatingFileHandler to cap file size, and many more.

Exceptions and secrets

Inside an except block, logger.exception("message") logs at ERROR level and appends the full traceback, so the log shows exactly where the failure happened:

Python
try:
    year = int(record["year"])
except (KeyError, ValueError):
    logger.exception("Could not parse year for record %s", record.get("id"))

Outside an except block there's no active exception, so use logger.error() instead, or pass exc_info=True explicitly.

Logs are copied, stored and read by many people and systems. Never log passwords, API keys, tokens, full card numbers or unnecessary personal data. Log identifiers (record id 1042) rather than contents (borrower name and address), mask secrets (key ...9f2c) and check what your __repr__ methods include, since objects are often logged whole. Data protection rules can apply to log files just as they do to databases.

Independent practice and a worked check

Task: add useful import progress and error logs with appropriate levels and no private record contents.

Python
import logging, sys
logging.basicConfig(stream=sys.stdout, level=logging.INFO, format="%(levelname)s %(message)s")
log = logging.getLogger("catalog")

borrowers = [{"id": 7, "email": "ana@example.com", "fines": "2.50"},
             {"id": 8, "email": "ben@example.com", "fines": "unpaid"}]
total = 0.0
for b in borrowers:
    try:
        total += float(b["fines"])
    except ValueError:
        log.exception("Bad fines value for borrower %s", b["id"])   # id, not email
log.info("Fines total: %.2f", total)

The ERROR line names borrower 8 and includes the traceback, without the email address. For the worked check, logger.exception() captures exception context inside an exception handler, where an exception is currently being handled.

Reference and next practice

The logging HOWTO and the logging cookbook cover configuration in depth. Continue with pytest in depth and revisit debugging. Practise with the Python exercises and Python quiz.

Logging Summary

  • Log levels: DEBUG, INFO, WARNING, ERROR and CRITICAL distinguish kinds of operational messages.
  • Logger names: Named loggers let application components be configured independently.
  • Deferred formatting: Passing a format string and arguments defers message interpolation until needed.
  • Exceptions and secrets: logger.exception records exception context inside an exception handler.

Logging Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Logging in a small program

Python
import logging
logging.basicConfig(level=logging.INFO, format="%(levelname)s:%(message)s")
logger = logging.getLogger("catalog")
logger.info("Imported %s shelves", 7)
Output
INFO:Imported 7 shelves
Line-by-line explanation
  1. Line 2basicConfig sets the minimum level to INFO and a simple line format; logging writes to standard error by default.
  2. Line 3A named logger for this part of the program.
  3. Line 4%s is filled in with 7 only because the INFO message is going to be shown.
Example 2

Levels, filtering and a logged exception

Python
import logging, sys

logging.basicConfig(stream=sys.stdout, level=logging.INFO,
                    format="%(levelname)-7s %(name)s: %(message)s")
logger = logging.getLogger("catalog.import")

records = [{"id": 1, "year": "1998"}, {"id": 2, "year": "n/a"}, {"id": 3}]
loaded = 0
logger.info("Import started with %d records", len(records))
for record in records:
    logger.debug("Processing %s", record)          # hidden at INFO level
    try:
        int(record["year"])
        loaded += 1
    except KeyError:
        logger.warning("Record %s has no year; skipped", record["id"])
    except ValueError:
        logger.error("Record %s has an invalid year", record["id"])
logger.info("Import finished: %d loaded, %d skipped", loaded, len(records) - loaded)
Output
INFO    catalog.import: Import started with 3 records
ERROR   catalog.import: Record 2 has an invalid year
WARNING catalog.import: Record 3 has no year; skipped
INFO    catalog.import: Import finished: 1 loaded, 2 skipped
Line-by-line explanation
  1. Lines 3 to 4stream=sys.stdout sends logs to standard output; %(levelname)-7s pads the level name for alignment.
  2. Line 11DEBUG is below the configured INFO level, so these messages never appear.
  3. Lines 15 to 18Different problems get different levels, so a search for ERROR finds only the invalid data.

Common Logging Mistakes

Logging a secret

Don't
logger.info("Connecting with API key %s", api_key)
Do
logger.info("Connecting with API key ending %s", api_key[-4:])

Anything in a log may be read by people and systems that should never see credentials.

Expecting INFO messages without configuring logging

Don't
import logging
logging.info("Import started")      # nothing appears: default level is WARNING
Do
import logging
logging.basicConfig(level=logging.INFO)
logging.info("Import started")

Logging in Real Projects

Logging in production systems

Servers and scheduled jobs run with nobody watching, so logs are how teams find out what happened. Web frameworks such as Django and Flask use the logging module, and their configuration usually lives in a dictionary passed to logging.config.dictConfig(). Containers typically log to standard output, where the platform collects messages; services often log JSON lines so tools like Elasticsearch, Datadog or CloudWatch can search fields such as level and request ID.

Good logs answer three questions after an incident: what was the program doing, what failed, and which record or request was involved, all without exposing private data.

Logging FAQ

Why use logging instead of print in Python?

Logging adds levels, timestamps and module names, can be filtered or redirected without code changes, and can write to files or monitoring services. print() is fine for quick checks and for a program's real output, but diagnostic messages belong in logs.

Why doesn't logging.info() print anything?

The default level is WARNING, so INFO and DEBUG messages are dropped. Call logging.basicConfig(level=logging.INFO) once at start-up, before any logging calls.

How do I log to a file in Python?

logging.basicConfig(filename="app.log", level=logging.INFO, encoding="utf-8") sends messages to a file. For long-running programs, add a RotatingFileHandler or TimedRotatingFileHandler so the file doesn't grow forever.

What is the difference between logger.error and logger.exception?

Both log at ERROR level. logger.exception() also includes the current exception's traceback, so it should be called inside an except block. logger.error() logs only the message unless you pass exc_info=True.

Should I use f-strings in log messages?

They work, but the %s argument style is recommended: the message is only formatted if it will be output, and the constant template makes logs easier to group and search. Linters such as Ruff and Pylint can flag f-strings in logging calls.

Logging Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choiceeasy

Where does logger.exception normally capture exception context?

Multiple choicemedium

Which review concern is valid for logging?

Logging Quiz

2 questions to check what you learned.

  1. Where does logger.exception normally capture exception context?

    • Before any exception occurs
    • Inside an exception handler
    • Python ignores the distinction
  2. Which statement correctly describes deferred formatting?

    • Passing a format string and arguments defers message interpolation until needed.
    • Only the example output matters, not the rule
    • Every operation has the same behavior