0%

49- Logging in Python

Replace print with professional logging. Track events, errors, and debug information. Essential for production applications and debugging.

You have used print() to debug your code. It works. But in production? When your application is running on a server? When you cannot see the console? When you need to know what happened yesterday at 3 AM? print() is not enough. It has no levels, no timestamps, no file output, no filtering. Professional logging solves these problems. Python’s built-in logging module is a complete logging system. You can log messages with different severity levels (DEBUG, INFO, WARNING, ERROR, CRITICAL). You can send logs to files, rotate them, format them with timestamps, and filter them by level. You can log from any module and configure everything from a central place. This lesson covers everything you need to know: logging levels, basic configuration, logging to files, formatting, handlers, and best practices for real-world applications.

🕯️ Magic Note

The logging module is one of Python’s most underrated features. It is thread-safe, highly configurable, and can handle millions of log messages efficiently. Most professional Python applications use it exclusively, not print().

Logging Levels
Logging levels indicate the severity of an event.
LevelNumeric ValueWhen to Use
DEBUG10Detailed information for diagnosing problems (development only)
INFO20Confirmation that things are working as expected
WARNING30Something unexpected happened, but the program is still working
ERROR40A more serious problem, the program cannot perform a specific function
CRITICAL50A serious error, the program may not be able to continue running

Python

import logging

# Basic configuration

logging.basicConfig(level=logging.DEBUG)

# Log messages at different levels

logging.debug(“Detailed debugging information”)

logging.info(“Informational message”)

logging.warning(“Warning: something unexpected”)

logging.error(“Error: something failed”)

logging.critical(“Critical: program may crash”)

# Output (depending on the level configured):

# DEBUG:root:Detailed debugging information

# INFO:root:Informational message

# WARNING:root:Warning: something unexpected

# ERROR:root:Error: something failed

# CRITICAL:root:Critical: program may crash

💡 Only messages at or above the configured level are shown. If you set level=logging.WARNING, only WARNING, ERROR, and CRITICAL messages will appear. DEBUG and INFO are ignored.
Basic Configuration: logging.basicConfig()
Configure the logging system with filename, level, format, and more.

Python

import logging

# Log to file instead of console

logging.basicConfig(filename=”app.log”, level=logging.INFO)

# Log with custom format (timestamp, level, message)

logging.basicConfig(

level=logging.DEBUG,

format=”%(asctime)s – %(name)s – %(levelname)s – %(message)s”,

datefmt=”%Y-%m-%d %H:%M:%S”

)

logging.info(“This has a timestamp!”)

# Output: 2025-05-11 14:30:45 – root – INFO – This has a timestamp!

🕯️ Magic Note

The basicConfig() function only works the first time it is called. After that, changes have no effect. Call it once at the beginning of your program, before any logging occurs.

Format Specifiers for Logs
Control exactly what information appears in each log message.
Format SpecifierMeaning
%(asctime)sHuman-readable time (when the log was created)
%(name)sName of the logger (usually the module name)
%(levelname)sLogging level (DEBUG, INFO, WARNING, etc.)
%(message)sThe log message itself
%(filename)sSource file name
%(lineno)dLine number in the source file
%(funcName)sFunction name where the log was called
%(pathname)sFull path of the source file
%(process)dProcess ID
%(thread)dThread ID
%(created)fTime when the log was created (as float)

Python

import logging

logging.basicConfig(

level=logging.DEBUG,

format=”[%(asctime)s] [%(levelname)s] [%(filename)s:%(lineno)d] %(message)s”,

datefmt=”%H:%M:%S”

)

def calculate(x, y):

logging.debug(f”Calculating {x} + {y}”)

return x + y

calculate(5, 3)

# Output: [14:30:45] [DEBUG] [script.py:8] Calculating 5 + 3

Creating Named Loggers (Best Practice)
Use named loggers instead of the root logger for better organization.

Python

import logging

# Create a logger for this module

logger = logging.getLogger(__name__)

# Configure (can be configured once at the top level)

logging.basicConfig(level=logging.DEBUG, format=”%(asctime)s – %(name)s – %(levelname)s – %(message)s”)

# Use the logger (not the root logger)

logger.debug(“This is a debug message from a named logger”)

logger.info(“This is an info message”)

logger.warning(“This is a warning”)

logger.error(“This is an error”)

# The name helps identify where the log came from

# 2025-05-11 14:30:45 – __main__ – DEBUG – This is a debug message

🕯️ Magic Note

Using logger = logging.getLogger(__name__) is the standard pattern. The logger name will be the module’s dotted path (e.g., “my_package.submodule”), making it easy to identify the source of each log message.

Logging to Files with Rotation
Use file handlers to write logs to files, with automatic rotation to manage size.

Python

import logging

from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler

# Remove the default configuration

logging.root.handlers = []

# Create a custom logger

logger = logging.getLogger(__name__)

logger.setLevel(logging.DEBUG)

# Rotating file handler: keep files under 1MB, keep 5 backups

file_handler = RotatingFileHandler(“app.log”, maxBytes=1_000_000, backupCount=5)

# Or time-based rotation: rotate daily, keep 7 days

# file_handler = TimedRotatingFileHandler(“app.log”, when=”midnight”, interval=1, backupCount=7)

# Set formatter

formatter = logging.Formatter(“%(asctime)s – %(name)s – %(levelname)s – %(message)s”)

file_handler.setFormatter(formatter)

# Add handler to logger

logger.addHandler(file_handler)

# Also log to console

console_handler = logging.StreamHandler()

console_handler.setLevel(logging.INFO)

console_handler.setFormatter(formatter)

logger.addHandler(console_handler)

logger.info(“This goes to both file and console”)

logger.debug(“This only appears in the file (since console handler is INFO)”)

💡 Always use rotating file handlers for production applications. Without rotation, log files can grow indefinitely and fill your disk. RotatingFileHandler creates rolling backups.
Setting Different Levels for Different Handlers
You can log DEBUG to a file but only INFO to the console.

Python

import logging

logger = logging.getLogger(__name__)

logger.setLevel(logging.DEBUG) # Master level (all handlers inherit this)

# File handler: log everything (DEBUG and above)

file_handler = logging.FileHandler(“app.log”)

file_handler.setLevel(logging.DEBUG)

# Console handler: only warnings and above

console_handler = logging.StreamHandler()

console_handler.setLevel(logging.WARNING)

# Formatter for both

formatter = logging.Formatter(“%(asctime)s – %(levelname)s – %(message)s”)

file_handler.setFormatter(formatter)

console_handler.setFormatter(formatter)

logger.addHandler(file_handler)

logger.addHandler(console_handler)

logger.debug(“This goes to file only”)

logger.info(“This goes to file only”)

logger.warning(“This goes to both file and console”)

logger.error(“This goes to both file and console”)

Logging Exceptions with exc_info
Log full exception tracebacks along with your messages.

Python

import logging

logger = logging.getLogger(__name__)

logging.basicConfig(level=logging.ERROR, format=”%(asctime)s – %(levelname)s – %(message)s”)

try:

result = 10 / 0

except ZeroDivisionError as e:

# Log the exception with full traceback

logger.error(“An error occurred”, exc_info=True)

# Or use exception() which is equivalent to error(…, exc_info=True)

try:

result = 10 / 0

except ZeroDivisionError as e:

logger.exception(“An error occurred”) # Automatically includes traceback

🕯️ Magic Note

Use logger.exception() inside exception handlers. It logs at ERROR level and automatically includes the full traceback. Your debugging will be much easier.

Logging Variable Data
Use formatting to include dynamic values in log messages.

Python

import logging

logger = logging.getLogger(__name__)

logging.basicConfig(level=logging.INFO, format=”%(asctime)s – %(levelname)s – %(message)s”)

name = “Feloriya”

score = 95

# Using f-strings (works but format string is evaluated even if log is disabled)

logger.info(f”User {name} scored {score}”)

# Better: use lazy formatting (format string only evaluated if level is enabled)

logger.info(“User %s scored %d”, name, score)

# Or using .format() style

logger.info(“User {} scored {}”.format(name, score))

💡 The lazy formatting style logger.info(“User %s scored %d”, name, score) is more efficient. If the log level is below INFO, the string is never formatted. This saves CPU cycles when debug logs are disabled.
Configuration File (logging.conf)
Complex logging configurations are better defined in a configuration file.

INI (logging.conf)

[loggers]

keys=root,myapp

[handlers]

keys=consoleHandler,fileHandler

[formatters]

keys=simpleFormatter

[logger_root]

level=WARNING

handlers=consoleHandler

[logger_myapp]

level=DEBUG

handlers=consoleHandler,fileHandler

qualname=myapp

propagate=0

[handler_consoleHandler]

class=StreamHandler

level=INFO

formatter=simpleFormatter

args=(sys.stdout,)

[handler_fileHandler]

class=handlers.RotatingFileHandler

level=DEBUG

formatter=simpleFormatter

args=(“app.log”, “a”, 1000000, 5)

[formatter_simpleFormatter]

format=%(asctime)s – %(name)s – %(levelname)s – %(message)s

datefmt=%Y-%m-%d %H:%M:%S

Python

import logging.config

# Load configuration from file

logging.config.fileConfig(“logging.conf”)

# Get a logger for your application

logger = logging.getLogger(“myapp”)

logger.debug(“This will go to the file only”)

logger.info(“This will go to both file and console”)

logger.warning(“This is a warning”)

Practical Example: Complete Web Application Logging
A realistic logging setup for a Flask application.

Python

import logging

from logging.handlers import RotatingFileHandler

import os

def setup_logging(app):

“””Configure logging for a Flask application.”””

# Create logs directory if it doesn’t exist

if not os.path.exists(“logs”):

os.makedirs(“logs”)

# Remove default handlers

app.logger.handlers = []

# Set log level based on environment

env = os.environ.get(“FLASK_ENV”, “production”)

if env == “development”:

log_level = logging.DEBUG

else:

log_level = logging.INFO

# File handler (all logs)

file_handler = RotatingFileHandler(“logs/app.log”, maxBytes=10_000_000, backupCount=10)

file_handler.setLevel(logging.DEBUG)

# Error file handler (only errors)

error_handler = RotatingFileHandler(“logs/error.log”, maxBytes=10_000_000, backupCount=10)

error_handler.setLevel(logging.ERROR)

# Console handler (for development)

console_handler = logging.StreamHandler()

console_handler.setLevel(log_level)

# Formatter

formatter = logging.Formatter(

“[%(asctime)s] [%(levelname)s] [%(name)s] [%(filename)s:%(lineno)d] %(message)s”

)

file_handler.setFormatter(formatter)

error_handler.setFormatter(formatter)

console_handler.setFormatter(formatter)

# Add handlers

app.logger.addHandler(file_handler)

app.logger.addHandler(error_handler)

app.logger.addHandler(console_handler)

app.logger.setLevel(logging.DEBUG)

app.logger.info(“Logging configured successfully”)

Print vs Logging: When to Use Which
Use print() When...Use logging When...
Quick debugging in a scriptProduction applications
One-off scripts you will deleteLong-running services
Learning / experimentingApplications deployed on servers
You need to see output immediatelyYou need to review logs later
Simple console output onlyYou need file output, rotation, timestamps
No need for severity levelsYou need error levels (DEBUG, INFO, ERROR)
Common Mistakes with Logging
  • Using print() instead of logging in production code
  • Calling basicConfig() after logging has already occurred
  • Using f-strings in log messages (formatting happens even if log is disabled)
  • Not setting up log rotation (files grow indefinitely)
  • Using the root logger directly instead of named loggers
  • Forgetting to configure logging before importing other modules
Check Your Understanding
  • What are the five logging levels in Python?
  • How do you create a named logger for the current module?
  • Write code that logs DEBUG to a file and INFO to the console.
  • How do you log an exception with full traceback?
  • Why should you avoid f-strings in log messages?
  • What is the purpose of RotatingFileHandler?

⚡ Whisper

Print is for the terminal. Logging is for the world. Print shouts into the void. Logging whispers to a file, a timestamp, a level. When your program runs on a server at 3 AM and crashes, print is silent. Logging tells you what happened. DEBUG for details. INFO for progress. WARNING for surprises. ERROR for failures. CRITICAL for disasters. Each level has a purpose. Each message has a format. Timestamps, filenames, line numbers. You can search them. You can filter them. You can rotate them. Logging is not complicated. It is basicConfig() and getLogger(__name__). It is logger.info() instead of print(). Start today. Replace your prints. Your future self will thank you. The server logs will tell the story. Read it.

Related posts