šŸš€ LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
šŸŽ“ COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.
HTML MASTER CLASS /// LEARN TAGS /// BUILD STRUCTURE /// SEMANTIC WEB /// HTML MASTER CLASS /// LEARN TAGS ///

Python logging Module

Loggers, handlers, formatters, and levels — the standard library's production-grade observability tool, and exactly why print() debugging doesn't survive contact with a real deployed system.

⚔ Total XP: 0|šŸ’» python XP: 0

Skill Matrix

UNLOCK NODES BY LEARNING NEW TAGS.

System Hub

Core logic.

Quick Quiz //

With logging.basicConfig(level=logging.INFO), why does logger.debug(...) not appear in the output?


šŸš€ LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
šŸŽ“ COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.

print() works fine on your laptop. In a deployed service, you need configurable severity levels, structured output, multiple destinations, and the ability to turn verbosity up or down without a code change — which is exactly what the logging module, composed of Loggers, Handlers, and Formatters, provides.

1Why print() Debugging Does Not Survive Production

print() has exactly one behavior: write text to standard output, unconditionally, every single time it's called. There's no concept of severity (is this a routine event or a critical failure?), no way to turn it off without editing and redeploying code, no way to send different messages to different destinations, and no structured metadata (timestamp, module name, log level) attached automatically. In a deployed service — running on a server you likely can't attach an interactive debugger to, generating output that needs to be searchable, filterable, and retained for a specific period — these gaps aren't theoretical inconveniences; they're the difference between diagnosing a 3am production incident in minutes versus hours.

The logging module's core abstraction directly addresses each gap: levels (DEBUG, INFO, WARNING, ERROR, CRITICAL, in increasing severity) let you tag every message with how important it is, and filter by a configurable threshold — logger.debug(...) calls can stay in your code permanently, silently inert in production, and instantly useful the moment you temporarily lower the threshold to DEBUG while diagnosing an issue, with zero code changes or redeployment required.

This single capability — toggle verbosity via configuration, not code changes — is often the single biggest practical reason professional codebases require logging over print(): it turns 'add some debug prints, deploy, reproduce the bug, remove the prints, redeploy' into 'flip a configuration flag', a difference measured in minutes versus a full deployment cycle.

āœ•
—
+
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

logger.debug("Detailed diagnostic info")   # filtered out -- below INFO
logger.info("Normal operation event")       # shown
logger.warning("Something unexpected")      # shown
logger.error("An operation failed")         # shown
logger.critical("System is unusable")       # shown
localhost:3000
Level Filtering
level=logging.INFO
DEBUG messages silently filtered; INFO and above shown

2getLogger(__name__): One Named Logger Per Module

logging.getLogger(__name__), called at the top of every module, creates (or retrieves, since getLogger is idempotent per name) a logger whose name matches that module's dotted import path — logging.getLogger(__name__) inside myproject/database/connection.py produces a logger named "myproject.database.connection". This is the standard, idiomatic pattern specifically because it builds a logger *hierarchy* mirroring your actual package structure, without any manual naming effort.

That hierarchy is directly useful for configuration: application startup code (or an ops team, without touching source code at all) can set myproject.database to DEBUG while everything else stays at WARNING, precisely targeting verbose output at the one subsystem currently under investigation, rather than being forced into an all-or-nothing global verbosity setting. This is something a single, undifferentiated print() (or even a single global logger) simply cannot express.

The root logger (logging.getLogger(), with no name argument) sits at the top of this hierarchy and is what logging.basicConfig() configures directly — every named logger, by default, propagates its messages up to the root logger's handlers unless explicitly told not to (logger.propagate = False), which is why a single basicConfig() call at application startup is often enough to get sensible output from every module's individually-named logger without configuring each one separately.

āœ•
—
+
# module_a.py
import logging
logger = logging.getLogger(__name__)  # __name__ = 'module_a'

# module_b.py
import logging
logger = logging.getLogger(__name__)  # __name__ = 'module_b'

# You can now set module_a to DEBUG and module_b to WARNING independently
localhost:3000
Logger Hierarchy
getLogger("myproject.database.connection")
Independently configurable, mirrors the module's import path

3Handlers and Formatters: One Logger, Multiple Destinations and Formats

A Handler determines *where* a log message actually goes — FileHandler writes to a file, StreamHandler writes to the console (stdout/stderr), and other handlers (not shown here) can send logs to syslog, a network socket, or a rotating set of files. A single logger can have *multiple* handlers attached simultaneously, and — critically — each handler can have its own independent level threshold, letting the same logger send verbose DEBUG-and-up output to a file for later analysis, while only surfacing WARNING-and-up on the console where a human is actively watching in real time.

A Formatter, attached to a specific handler (not to the logger directly), controls the actual text layout of each message — "%(asctime)s %(levelname)s %(message)s" produces output like 2026-08-10 14:30:00 INFO Normal operation event, with the format string's placeholders filled from the log record's metadata (timestamp, level name, the message itself, and several other available fields like %(name)s for the logger's name or %(filename)s/%(lineno)d for source location). Different handlers can use entirely different formatters — a file handler might include full timestamps and module paths for later grep-based searching, while a console handler shows a terser, more human-readable format.

This Logger → Handler → Formatter composition is precisely the design this module's earlier Composition vs Inheritance lesson used as its real-world validation example — logging.Logger doesn't inherit its output-destination or formatting behavior; it *holds* a list of Handler objects, each independently configured, each optionally holding its own Formatter — exactly the composed, independently-swappable design that lesson argued for, implemented directly in the standard library you use every day.

āœ•
—
+
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)

file_handler = logging.FileHandler("app.log")
file_handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))

console_handler = logging.StreamHandler()
console_handler.setLevel(logging.WARNING)  # console only sees WARNING+

logger.addHandler(file_handler)
logger.addHandler(console_handler)
logger.info("Written to file only")
logger.error("Written to BOTH file and console")
localhost:3000
Composed Output
One logger, two handlers
File gets everything DEBUG+; console gets only WARNING+

4Step-by-Step Breakdown

print() debugging works until your code runs on a server you can't attach a debugger to at 3am. logging is what replaces it, permanently.

logging has five severity LEVELS -- a call at each level can be filtered independently, unlike print() which has no concept of severity at all.

Checkpoint: With logging.basicConfig(level=logging.INFO), why does logger.debug(...) not appear in the output?

  • →DEBUG is a lower severity than the configured INFO threshold, so it is filtered out
  • →debug() is not a valid logging method — only info/warning/error/critical exist

logging.getLogger(__name__) -- NOT the root logger -- is the standard pattern, giving every module its own named logger you can configure independently.

Checkpoint: Why is logging.getLogger(__name__) preferred over logging.getLogger() (the root logger) in library/application code?

  • →It gives each module its own named logger, letting different parts of a codebase be configured (verbosity, handlers) independently
  • →Named loggers are significantly faster than the root logger

Handlers and Formatters let ONE logger send output to MULTIPLE destinations, each formatted differently -- something print() has no equivalent for at all.

That completes Advanced Standard Library — pathlib, collections, itertools, functools, datetime, and logging cover the modules every professional Python codebase leans on daily. Next, argparse rounds out the practical toolkit with command-line interfaces.

Reuse a Real Named Logger. Finish get_module_logger(): getLogger(name) always returns the same instance for the same name.

Level Up šŸš€

Advanced cheat sheets, SEO tricks, and interview prep for this topic.

Browser Support

ChromeSupported

Fully supported (via server-side Python execution).

FirefoxSupported

Fully supported (via server-side Python execution).

SafariSupported

Fully supported (via server-side Python execution).

EdgeSupported

Fully supported (via server-side Python execution).

Best Practices

Use logging.getLogger(__name__) in every module, never the bare root logger directly

This builds a hierarchy matching your package structure, letting verbosity be configured per-module rather than forcing an all-or-nothing global setting.

Leave debug-level logging calls in production code rather than removing them after debugging

Since logger.debug() calls are silently filtered by default level thresholds, they cost nothing when inactive and are immediately available the next time you need to raise verbosity to diagnose an issue — no code change or redeploy required.

Frequent Bugs

THE BUG

Using print() statements scattered throughout production code for debugging, which then either clutter production output permanently or require a code change and redeploy to remove.

THE FIX

Replace print() with logger.debug()/logger.info() calls using an appropriately-scoped logger — they can be toggled on/off via configuration without any code changes, and integrate with severity filtering and structured output.

Real-World Examples

Configuring Different Verbosity for a Noisy Third-Party Library

An application's own code needs DEBUG-level logging during development, but a noisy third-party HTTP library it depends on floods the output with DEBUG messages that aren't currently useful.

import logging

logging.basicConfig(level=logging.DEBUG)  # app-wide default

# Quiet down the noisy third-party library specifically
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("httpcore").setLevel(logging.WARNING)

# Our own code's DEBUG logs still show up normally
logger = logging.getLogger(__name__)
logger.debug("This still appears")

Interview Prep

?Frequently Asked Questions

Pascual Vila

Pascual Vila

Frontend Instructor // Code Syllabus

Common Pitfalls & Errors

The Error //

Using print() throughout an application for status/error messages, which cannot be filtered by severity or redirected to different destinations without editing the source code.

# Wrong: no severity, no configurability without code changes print("Starting process...") print(f"ERROR: {error}") # Correct: filterable, configurable, structured import logging logger = logging.getLogger(__name__) logger.info("Starting process...") logger.error(f"Process failed: {error}")

The Solution //

Replace print() calls with logger = logging.getLogger(__name__) and appropriate logger.debug()/.info()/.warning()/.error() calls, configurable via logging.basicConfig() or a dictConfig() without touching the call sites themselves.

Lesson Glossary

[01]Logger

The primary logging.getLogger(__name__)-created object used to emit log messages at various severity levels.

Code Preview
// Logger context

[02]Handler

An object attached to a Logger that determines where log messages are sent (file, console, network) and at what threshold.

Code Preview
// Handler context

[03]Formatter

An object attached to a Handler that controls the text layout of each log message, using placeholders like %(asctime)s and %(levelname)s.

Code Preview
// Formatter context

[04]Log level

A severity classification (DEBUG, INFO, WARNING, ERROR, CRITICAL) used to filter which messages are actually emitted.

Code Preview
// Log level context

Continue Learning