Focused practice · 9 min read

Python Logging Exercises: Levels, Handlers, Format

Print statements disappear when you need context. Logging gives each message a severity, a destination, and a format, which makes production behavior easier to inspect without changing every call.

Get a named logger

Use logging.getLogger with a stable name such as the module name. Named loggers form a hierarchy, so configuration can be shared. Avoid creating a new logger for every function call.

import logging
logger = logging.getLogger("app")
logger.setLevel(logging.INFO)
print(logger.name)

One logger per module

Levels filter records

A logger only handles records at or above its level. INFO is below WARNING, so an info call is ignored when the logger is set to WARNING. Set levels deliberately instead of lowering everything to DEBUG.

import logging
from io import StringIO
stream = StringIO()
logger = logging.getLogger("app")
logger.handlers[:] = [logging.StreamHandler(stream)]
logger.setLevel(logging.WARNING)
logger.propagate = False
logger.info("hidden")
logger.warning("shown")
print(stream.getvalue().strip())

Below the threshold

Handlers choose destinations

A handler writes to a stream, file, or another destination. StreamHandler is useful for the console and for tests that capture a StringIO. Add handlers explicitly when you need control over where records go.

import logging
from io import StringIO
stream = StringIO()
logger = logging.getLogger("demo")
logger.handlers[:] = [logging.StreamHandler(stream)]
logger.setLevel(logging.INFO)
logger.propagate = False
logger.info("hello")
print(stream.getvalue().strip())

A handler with a testable stream

Formatters control the text

A formatter turns a log record into the final string. The format string can include the level, logger name, message, and other record attributes. Keep the format stable so logs remain searchable.

import logging
from io import StringIO
stream = StringIO()
handler = logging.StreamHandler(stream)
handler.setFormatter(logging.Formatter("%(levelname)s:%(message)s"))
logger = logging.getLogger("demo")
logger.handlers[:] = [handler]
logger.setLevel(logging.INFO)
logger.propagate = False
logger.warning("check")
print(stream.getvalue().strip())

Level and message

Stop propagation when you own the output

Records travel up the logger hierarchy to ancestor handlers. That can duplicate output when both a local handler and the root logger are configured. Set propagate to False for a self-contained logger.

import logging
from io import StringIO
stream = StringIO()
logger = logging.getLogger("demo")
logger.handlers[:] = [logging.StreamHandler(stream)]
logger.setLevel(logging.INFO)
logger.propagate = False
logger.info("once")
print(stream.getvalue().strip())

One record, one destination

Your practice checklist

Common questions

Should I use print or logging?

Use logging for messages that may need severity, filtering, or a different destination. Print is fine for tiny scripts and immediate debugging.

Why did my log message not appear?

Check the logger level, the handler level, and whether a handler is attached. A level above the record's severity filters it out.

Can I test logging output?

Yes. Attach a StreamHandler to a StringIO, set propagate to False, call the logger, and assert on the captured text.

Practice logging

12 questions on levels, handlers, formatters, propagation, and filter behavior.

Open practice

Keep reading: