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
- Use getLogger with a stable module-level name.
- Set the logger level to the lowest severity you need.
- Attach a handler for each destination.
- Use a formatter when the default text is not enough.
- Set propagate=False when a local handler should own the output.
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.
Keep reading: