The definitive guide. Start with the "Basic Logging Tutorial" section. Also essential: Logging Cookbook.
// Console levels exist but are cosmetic
console.log("info message");
console.warn("warning");
console.error("error");
console.debug("debug");
// Production: use a library like pino, winston
import pino from "pino";
const log = pino({ level: "info" });
log.info({ userId: 42 }, "User logged in");
log.error({ err }, "Something broke");
import logging
# Levels: DEBUG < INFO < WARNING < ERROR < CRITICAL
logging.debug("Low-level detail")
logging.info("Normal operation")
logging.warning("Something unexpected")
logging.error("Something failed")
logging.critical("Application cannot continue")
# Each level has a numeric value:
# DEBUG=10, INFO=20, WARNING=30, ERROR=40, CRITICAL=50
Unlike print(), the logging module gives you: level filtering, timestamps, caller info, multiple output destinations (file + console), and the ability to silence library noise — all configurable without changing code.
import logging
# Minimal setup — call once, at the top of your main script
logging.basicConfig(
level=logging.DEBUG, # show all levels ≥ DEBUG
format="%(asctime)s %(levelname)-8s %(name)s — %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
# Now use it
logging.info("Server started on port 8080")
logging.warning("Config file not found, using defaults")
logging.error("Database connection failed")
# Output:
# 2026-06-23 22:30:01 INFO root — Server started on port 8080
# 2026-06-23 22:30:01 WARNING root — Config file not found, using defaults
# 2026-06-23 22:30:01 ERROR root — Database connection failed
Always use logging.getLogger(__name__) in your modules — not the root logger. This makes it easy to silence or configure specific parts of your app.
# In every module file:
import logging
logger = logging.getLogger(__name__)
# __name__ = "myapp.database" if file is myapp/database.py
def connect(url: str):
logger.info("Connecting to %s", url) # use %s, not f-strings (lazy formatting!)
try:
conn = db.connect(url)
logger.debug("Connection established in %.2fms", elapsed)
return conn
except Exception as e:
logger.exception("Failed to connect to %s", url) # logs ERROR + full traceback!
raise
Write logger.info("User %s logged in", user_id) not logger.info(f"User {user_id} logged in"). With %, the string is only formatted if the message will actually be logged — saving CPU on DEBUG messages in production.
Handlers send log records to destinations. You can attach multiple handlers — e.g. console + file simultaneously.
import logging
import sys
def setup_logging(level: str = "INFO") -> None:
"""Call once from your main() function."""
root = logging.getLogger()
root.setLevel(logging.DEBUG) # capture everything; handlers filter
# Console handler — INFO and above
console = logging.StreamHandler(sys.stdout)
console.setLevel(getattr(logging, level.upper()))
console.setFormatter(logging.Formatter(
"%(asctime)s %(levelname)-8s %(name)s — %(message)s",
datefmt="%H:%M:%S"
))
# File handler — DEBUG and above (full detail)
file_h = logging.FileHandler("app.log", encoding="utf-8")
file_h.setLevel(logging.DEBUG)
file_h.setFormatter(logging.Formatter(
"%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)d — %(message)s"
))
root.addHandler(console)
root.addHandler(file_h)
# main.py
if __name__ == "__main__":
setup_logging("INFO")
main()
# Third-party libraries (requests, httpx, etc.) use logging too.
# You often want to silence their verbose DEBUG output.
import logging
# Silence specific library loggers
logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("asyncio").setLevel(logging.WARNING)
# Or silence everything below WARNING globally, except your own code
logging.getLogger().setLevel(logging.WARNING)
logging.getLogger("myapp").setLevel(logging.DEBUG) # your app: full detail
For production systems, use structlog or python-json-logger for JSON output that log aggregators (Datadog, CloudWatch, etc.) can parse.
pip install structlog
import structlog
log = structlog.get_logger()
log.info("user.login", user_id=42, ip="192.168.1.1")
# {"event": "user.login", "user_id": 42, "ip": "192.168.1.1", "timestamp": "..."}
log.error("payment.failed", order_id="ORD-99", amount=99.99, error="Card declined")
# Machine-readable JSON — perfect for log aggregation
| Specifier | Meaning |
|---|---|
%(asctime)s | Timestamp |
%(levelname)s | DEBUG / INFO / WARNING etc. |
%(name)s | Logger name (__name__) |
%(message)s | The log message |
%(filename)s | Source file |
%(lineno)d | Line number |
%(funcName)s | Function name |
1. What does logger.exception("msg") log compared to logger.error("msg")?
2. Why use logger.info("User %s", uid) instead of logger.info(f"User {uid}")?
3. You want DEBUG in your app's log file but only WARNING on the console. How?
Questions answered correctly.