Print statements can show what a running application is doing, but they are awkward to leave in place. Python’s logging module provides messages that can be formatted consistently, filtered by severity, and routed to an output such as STDOUT. Timestamped messages also help reconstruct the order of events when debugging. section 7.8 section 12.2.3
import logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
ch.setFormatter(formatter)
logger.addHandler(ch)
try:
x = 10 / 0
except Exception as e:
logger.error("something bad happened")
A small setup creates a logger, sets levels, attaches a stream handler, and gives messages a format. The logger can then emit an error from an exception handler. section 5.2.4
Log an exception when its details will help someone investigate it; do not use logging as a substitute for handling it. If an expected error can be handled locally with a useful message to the user, an additional log may be unnecessary. Conversely, silently catching an exception leaves neither the user nor a maintainer with a useful result. section 5.2.4
The main configuration choices are:
- Level: DEBUG messages can remain in the code while an INFO production setting filters them out. section 7.8.1
- Formatter: fields such as time, severity, module, and message make events easier to place in context. section 5.2.4 section 7.8.1
- Handler: choose where messages go. STDOUT suits the web application example; other destinations can keep diagnostics separate from user output. section 7.8 section 5.2.4
version: 1
disable_existing_loggers: true #1
formatters:
default:
format: '[%(asctime)s.%(msecs)03d]
%(levelname)s in %(module)s: %(message)s'
datefmt: '%Y-%m-%d %H:%M:%S'
handlers:
console: #2
level: DEBUG #2
class: logging.StreamHandler #2
formatter: default #2
stream: ext://sys.stdout #2
loggers:
'':
level: DEBUG
handlers: [console]
propagate: false
This YAML configuration puts the formatter, STDOUT stream handler, and logger in one place. Application startup loads the file into a dictionary, adjusts levels for the environment, and applies it with logging.config.dictConfig(). section 7.8.1
logger = getLogger(__file__)
@intro_bp.route("/")
def home():
logger.debug("rendering home page") #1
return render_template("index.html", data={
"now": datetime.now(),
"page_visit": PageVisit(),
"banner_colors": BannerColors().get_colors()
})
@intro_bp.route("/about")
def about():
logger.debug("rendering about page") #2
return render_template("about.html")
Route-level DEBUG messages record which page is being rendered. They help during development without requiring those messages to appear when the application runs at INFO level. section 7.8.1
The practical payoff is visibility without permanent print-statement clutter: keep useful DEBUG calls, select an appropriate level for each environment, and turn them back on when a new problem needs investigation. section 12.2.3



