Mastering Logging in Python – DEV Community


When I first started writing Python code, I relied almost entirely on print() statements to figure out what my program was doing. It worked, at least until my projects got bigger. Once I began building real applications with multiple modules, background tasks, and user interactions, the flood of print() outputs became a nightmare to manage. That’s when I realized I needed something more structured, more scalable, I needed logging.

Logging is essentially your program’s way of keeping a journal. Instead of dumping random text to the console, you record meaningful messages that describe what your application is doing, when it’s doing it, and, most importantly, what went wrong when things break. A well-placed log message can save you hours of debugging.

The power of logging it’s not just about tracking what’s happening right now, it’s about creating a reliable record of your application’s behaviour over time. Once you start using Python’s logging module, you’ll never want to go back to print() debugging again.

Source code for all examples available at the end of the article.




What Is Logging?

At its core, logging is the process of recording events that happen while your program runs. Think of it as your application’s black box recorder, it captures important information about what the code is doing, when it’s doing it, and what happens if something goes wrong.

In software development, logging plays a crucial role in application monitoring and troubleshooting. It helps developers understand the behaviour of their systems in real-time, detect performance issues, track user actions, and diagnose bugs that occur in production, all without interrupting the application’s flow.

To understand logging better, it helps to break it down into a few key concepts:



Log Messages

A log message is the core piece of information you record. It can describe anything, from a simple “server started” message to a detailed stack trace of an exception. Each message gives context to what’s happening inside your code.



Log Levels

Log levels indicate the severity or importance of a message. Python’s logging module provides several built-in levels, such as:

  • DEBUG – Detailed information for diagnosing problems (useful during development).
  • INFO – Confirmation that things are working as expected.
  • WARNING – Something unexpected happened, but the program can continue.
  • ERROR – A serious issue that prevented part of the program from functioning.
  • CRITICAL – A severe error indicating the program may not be able to continue running.

Using the right log level helps you filter and prioritize messages, for example, showing only errors in production but everything in development.



Log Handlers and Formatters

Handlers determine where your logs go, the console, a file, a network socket, or even an external service like Sentry or CloudWatch.

Formatters define how your logs look, whether you include timestamps, file names, line numbers, or log levels in each message.



Log Files

Instead of just displaying messages in the terminal, logs can be written to files. This is essential for production systems, where you need a persistent record of events.

Log files make it possible to analyse patterns over time, investigate past incidents, and audit system activity.




Getting Started with Python’s logging Module

The beauty of Python’s logging module is that it’s built into the standard library, no installation required. You can start using it in just a few lines of code, and it immediately gives you more structure and control than print() statements ever could.

Here’s the simplest way to get started:

import logging

logging.basicConfig(level=logging.INFO)
logging.info("Application started")
logging.warning("This is a warning")
logging.error("An error occurred")
Enter fullscreen mode

Exit fullscreen mode

When you run this code, you’ll see something like:

INFO:root:Application started
WARNING:root:This is a warning
ERROR:root:An error occurred
Enter fullscreen mode

Exit fullscreen mode

Let’s break down what’s happening:

  • import logging loads Python’s built-in logging module.
  • basicConfig() sets up a default configuration, here we’re setting the minimum level to INFO.
  • logging.info(), logging.warning(), and logging.error() are methods that record messages at different severity levels.

By default, messages are sent to the console (stderr), but you can easily change that later to write logs to a file or even a remote logging service.

Want to sharpen your Python skills even more? Grab my free Python One-Liner Cheat Sheet, a quick reference packed with elegant, time-saving tricks and idiomatic expressions to make your code cleaner and faster. Download it here




Configuring Logging Output

As mentioned, by default Python’s logging system outputs messages to the console, but you can customize how your logs look and where they go. With just a few configuration tweaks, you can make your logs more readable, structured, and production-ready.



Setting Log Format

Readable logs are invaluable when debugging or monitoring applications in production.

Fortunately, Python’s logging module lets you define exactly how your log messages appear using the format and datefmt parameters in basicConfig().

Here’s an example that includes timestamps, the logger’s name, log level, and message content:

import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S"
)

logging.info("Server started")
logging.warning("Low disk space")
logging.error("Failed to connect to database")
Enter fullscreen mode

Exit fullscreen mode

Output:

2025-11-03 10:45:32 - root - INFO - Server started
2025-11-03 10:45:32 - root - WARNING - Low disk space
2025-11-03 10:45:32 - root - ERROR - Failed to connect to database
Enter fullscreen mode

Exit fullscreen mode

Let’s break down what each placeholder means:

  • %(asctime)s → Timestamp of when the log was recorded.
  • %(name)s → The name of the logger (by default it’s root).
  • %(levelname)s → The severity level of the message (INFO, WARNING, etc.).
  • %(message)s → The actual log message.

Formatting your logs like this makes them easier to scan and filter later, especially when multiple systems or modules are logging simultaneously.



Writing Logs to a File

While console logs are fine for development, production environments need persistent logs that can be reviewed later, for example, to analyze errors or audit user actions.

You can easily save logs to a file by specifying the filename parameter in basicConfig():

import logging

logging.basicConfig(
    filename="app.log",
    level=logging.INFO,
    format="%(asctime)s - %(levelname)s - %(message)s"
)

logging.info("Application started")
logging.error("Database connection failed")
Enter fullscreen mode

Exit fullscreen mode

This will create a file named app.log in the current directory and append logs to it.

2025-11-04 08:53:58,284 - INFO - Application started
2025-11-04 08:53:58,284 - ERROR - Database connection failed
Enter fullscreen mode

Exit fullscreen mode



Rotating Log Files

Over time, log files can grow very large and become unmanageable. To prevent this, Python provides rotating file handlers, which automatically create new log files when the current one reaches a certain size.

Here’s how to use RotatingFileHandler:

import logging
from logging.handlers import RotatingFileHandler

# Create a rotating file handler
handler = RotatingFileHandler(
    "app.log", maxBytes=2000, backupCount=3
)

# Configure the logger
logging.basicConfig(
    level=logging.INFO,
    handlers=[handler],
    format="%(asctime)s - %(levelname)s - %(message)s"
)

for i in range(100):
    logging.info(f"Log message {i}")
Enter fullscreen mode

Exit fullscreen mode

In this example:

  • maxBytes=2000 means each log file will be limited to about 2 KB.
  • backupCount=3 means up to 3 old log files will be kept (e.g., app.log.1, app.log.2, app.log.3).

This ensures your logs don’t consume unnecessary disk space, while still retaining enough history for debugging or analysis.

This will create a file named app.log:

2025-11-04 08:55:16,582 - INFO - Log message 78
2025-11-04 08:55:16,585 - INFO - Log message 79
2025-11-04 08:55:16,585 - INFO - Log message 80
2025-11-04 08:55:16,585 - INFO - Log message 81
2025-11-04 08:55:16,585 - INFO - Log message 82
2025-11-04 08:55:16,586 - INFO - Log message 83
2025-11-04 08:55:16,586 - INFO - Log message 84
2025-11-04 08:55:16,586 - INFO - Log message 85
2025-11-04 08:55:16,586 - INFO - Log message 86
2025-11-04 08:55:16,586 - INFO - Log message 87
2025-11-04 08:55:16,586 - INFO - Log message 88
2025-11-04 08:55:16,586 - INFO - Log message 89
2025-11-04 08:55:16,586 - INFO - Log message 90
2025-11-04 08:55:16,586 - INFO - Log message 91
2025-11-04 08:55:16,586 - INFO - Log message 92
2025-11-04 08:55:16,588 - INFO - Log message 93
2025-11-04 08:55:16,588 - INFO - Log message 94
2025-11-04 08:55:16,588 - INFO - Log message 95
2025-11-04 08:55:16,589 - INFO - Log message 96
2025-11-04 08:55:16,589 - INFO - Log message 97
2025-11-04 08:55:16,589 - INFO - Log message 98
2025-11-04 08:55:16,589 - INFO - Log message 99
Enter fullscreen mode

Exit fullscreen mode

And another file named app.log.1:

2025-11-04 08:55:16,575 - INFO - Log message 38
2025-11-04 08:55:16,577 - INFO - Log message 39
2025-11-04 08:55:16,577 - INFO - Log message 40
2025-11-04 08:55:16,578 - INFO - Log message 41
2025-11-04 08:55:16,578 - INFO - Log message 42
2025-11-04 08:55:16,578 - INFO - Log message 43
2025-11-04 08:55:16,578 - INFO - Log message 44
2025-11-04 08:55:16,578 - INFO - Log message 45
2025-11-04 08:55:16,578 - INFO - Log message 46
2025-11-04 08:55:16,578 - INFO - Log message 47
2025-11-04 08:55:16,578 - INFO - Log message 48
2025-11-04 08:55:16,579 - INFO - Log message 49
2025-11-04 08:55:16,579 - INFO - Log message 50
2025-11-04 08:55:16,579 - INFO - Log message 51
2025-11-04 08:55:16,579 - INFO - Log message 52
2025-11-04 08:55:16,579 - INFO - Log message 53
2025-11-04 08:55:16,580 - INFO - Log message 54
2025-11-04 08:55:16,580 - INFO - Log message 55
2025-11-04 08:55:16,580 - INFO - Log message 56
2025-11-04 08:55:16,580 - INFO - Log message 57
2025-11-04 08:55:16,580 - INFO - Log message 58
2025-11-04 08:55:16,580 - INFO - Log message 59
2025-11-04 08:55:16,581 - INFO - Log message 60
2025-11-04 08:55:16,581 - INFO - Log message 61
2025-11-04 08:55:16,581 - INFO - Log message 62
2025-11-04 08:55:16,581 - INFO - Log message 63
2025-11-04 08:55:16,581 - INFO - Log message 64
2025-11-04 08:55:16,581 - INFO - Log message 65
2025-11-04 08:55:16,581 - INFO - Log message 66
2025-11-04 08:55:16,581 - INFO - Log message 67
2025-11-04 08:55:16,581 - INFO - Log message 68
2025-11-04 08:55:16,581 - INFO - Log message 69
2025-11-04 08:55:16,581 - INFO - Log message 70
2025-11-04 08:55:16,581 - INFO - Log message 71
2025-11-04 08:55:16,581 - INFO - Log message 72
2025-11-04 08:55:16,581 - INFO - Log message 73
2025-11-04 08:55:16,582 - INFO - Log message 74
2025-11-04 08:55:16,582 - INFO - Log message 75
2025-11-04 08:55:16,582 - INFO - Log message 76
2025-11-04 08:55:16,582 - INFO - Log message 77
Enter fullscreen mode

Exit fullscreen mode

And another file called app.log.2:

2025-11-04 08:53:58,284 - INFO - Application started
2025-11-04 08:53:58,284 - ERROR - Database connection failed
2025-11-04 08:55:16,562 - INFO - Log message 0
2025-11-04 08:55:16,563 - INFO - Log message 1
2025-11-04 08:55:16,563 - INFO - Log message 2
2025-11-04 08:55:16,563 - INFO - Log message 3
2025-11-04 08:55:16,564 - INFO - Log message 4
2025-11-04 08:55:16,564 - INFO - Log message 5
2025-11-04 08:55:16,564 - INFO - Log message 6
2025-11-04 08:55:16,564 - INFO - Log message 7
2025-11-04 08:55:16,564 - INFO - Log message 8
2025-11-04 08:55:16,564 - INFO - Log message 9
2025-11-04 08:55:16,564 - INFO - Log message 10
2025-11-04 08:55:16,564 - INFO - Log message 11
2025-11-04 08:55:16,564 - INFO - Log message 12
2025-11-04 08:55:16,573 - INFO - Log message 13
2025-11-04 08:55:16,573 - INFO - Log message 14
2025-11-04 08:55:16,573 - INFO - Log message 15
2025-11-04 08:55:16,574 - INFO - Log message 16
2025-11-04 08:55:16,574 - INFO - Log message 17
2025-11-04 08:55:16,574 - INFO - Log message 18
2025-11-04 08:55:16,574 - INFO - Log message 19
2025-11-04 08:55:16,574 - INFO - Log message 20
2025-11-04 08:55:16,574 - INFO - Log message 21
2025-11-04 08:55:16,574 - INFO - Log message 22
2025-11-04 08:55:16,574 - INFO - Log message 23
2025-11-04 08:55:16,574 - INFO - Log message 24
2025-11-04 08:55:16,574 - INFO - Log message 25
2025-11-04 08:55:16,574 - INFO - Log message 26
2025-11-04 08:55:16,574 - INFO - Log message 27
2025-11-04 08:55:16,574 - INFO - Log message 28
2025-11-04 08:55:16,574 - INFO - Log message 29
2025-11-04 08:55:16,574 - INFO - Log message 30
2025-11-04 08:55:16,574 - INFO - Log message 31
2025-11-04 08:55:16,575 - INFO - Log message 32
2025-11-04 08:55:16,575 - INFO - Log message 33
2025-11-04 08:55:16,575 - INFO - Log message 34
2025-11-04 08:55:16,575 - INFO - Log message 35
2025-11-04 08:55:16,575 - INFO - Log message 36
2025-11-04 08:55:16,575 - INFO - Log message 37
Enter fullscreen mode

Exit fullscreen mode

You’ll notice that app.log contains the most recent logs, while app.log.1 and higher files store the older, rotated logs.




Creating Custom Loggers

Up to this point, we’ve been using the root logger, the default logger that Python creates when you call functions like logging.info() or logging.error() without explicitly naming a logger.

While this is fine for small scripts, it quickly becomes limiting as your application grows.

That’s where custom loggers come in.



Why and When to Use Custom Loggers

Custom loggers give you fine-grained control over how different parts of your application log information. For example, in a large project, you might have separate modules for authentication, database operations, and API handling.

Each of these can have its own logger, allowing you to:

  • Control the logging level per module (e.g., detailed logs for debugging one area while keeping others quiet).
  • Apply different handlers (e.g., send database logs to a file, but authentication logs to an alerting system).
  • Easily identify which module produced a particular log message.

Custom loggers make your logging modular, organized, and scalable.



Example

Here’s how to create and configure a custom logger:

import logging

# Create a custom logger
logger = logging.getLogger("my_app")
logger.setLevel(logging.DEBUG)

# Create a console handler
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)

# Define a formatter and add it to the handler
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
console_handler.setFormatter(formatter)

# Add the handler to the logger
logger.addHandler(console_handler)

# Example log messages
logger.debug("Debugging application startup")
logger.info("Application initialized successfully")
logger.warning("Low disk space")
Enter fullscreen mode

Exit fullscreen mode

Output:

2025-11-03 11:22:17 - my_app - INFO - Application initialized successfully
2025-11-03 11:22:17 - my_app - WARNING - Low disk space
Enter fullscreen mode

Exit fullscreen mode

Notice that:

  • The logger’s name (my_app) appears in the log messages, making it easy to trace which part of the code produced the message.
  • We can set different levels for the logger (DEBUG) and the handler (INFO), giving us flexible control over what gets displayed or recorded.



Propagation and Hierarchy of Loggers

Python’s logging system uses a hierarchical naming convention. For example:

  • A logger named my_app is the parent of my_app.database or my_app.api.
  • Child loggers automatically inherit settings (like handlers and levels) from their parents unless you explicitly override them.

This behavior is called propagation, it means that when a child logger emits a log message, that message “bubbles up” to its parent loggers unless propagation is disabled.

Example:

import logging

# Parent logger
parent_logger = logging.getLogger("my_app")
parent_logger.setLevel(logging.INFO)

# Create a console handler for the parent logger
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)

# Create a formatter
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
console_handler.setFormatter(formatter)

# Add handler to parent logger (child loggers will inherit it)
parent_logger.addHandler(console_handler)

# Child logger
child_logger = logging.getLogger("my_app.database")

child_logger.info("Connected to database")
Enter fullscreen mode

Exit fullscreen mode

By default, this message will appear in both child_logger and parent_logger outputs because it propagates upward.

2025-11-04 09:04:01,166 - my_app.database - INFO - Connected to database
Enter fullscreen mode

Exit fullscreen mode

You can turn propagation off if you want to isolate logs from certain parts of your system:

child_logger.propagate = False
Enter fullscreen mode

Exit fullscreen mode




Using Handlers, Formatters, and Filters

When you start working with more complex applications, you’ll often need your logs to go to multiple places, like the console for quick feedback and a file for long-term storage. You may also want to customize how messages look and filter which ones get recorded.

That’s where handlers, formatters, and filters come in. Together, they give you fine-grained control over how your logs behave and appear.



Handlers

Handlers are responsible for deciding where your log messages go. You can attach multiple handlers to a single logger, and each handler can send logs to a different destination.

Common handlers include:

  • StreamHandler — sends logs to streams like sys.stdout or sys.stderr (used for console output).
  • FileHandler — writes logs to a specified file.
  • RotatingFileHandler — automatically rotates log files when they reach a certain size (useful in production).
  • TimedRotatingFileHandler — rotates logs based on time intervals (e.g., daily).
  • SMTPHandler — sends log messages via email (useful for critical alerts).
  • HTTPHandler, SocketHandler, and others — send logs over the network.

Here’s an example using multiple handlers, one for console output and one for file logging:

import logging
from logging.handlers import RotatingFileHandler

# Create a logger
logger = logging.getLogger("multi_handler_app")
logger.setLevel(logging.DEBUG)

# Create handlers
console_handler = logging.StreamHandler()
file_handler = RotatingFileHandler("app.log", maxBytes=5000, backupCount=3)

# Set levels for each handler
console_handler.setLevel(logging.INFO)
file_handler.setLevel(logging.DEBUG)

# Define formatters
console_format = logging.Formatter("%(levelname)s - %(message)s")
file_format = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")

# Attach formatters to handlers
console_handler.setFormatter(console_format)
file_handler.setFormatter(file_format)

# Add handlers to logger
logger.addHandler(console_handler)
logger.addHandler(file_handler)

# Log some messages
logger.debug("Debug message (file only)")
logger.info("Info message (console + file)")
logger.error("Error message (console + file)")
Enter fullscreen mode

Exit fullscreen mode

Output (console):

INFO - Info message (console + file)
ERROR - Error message (console + file)
Enter fullscreen mode

Exit fullscreen mode

Output (in app.log):

2025-11-03 13:15:44 - multi_handler_app - DEBUG - Debug message (file only)
2025-11-03 13:15:44 - multi_handler_app - INFO - Info message (console + file)
2025-11-03 13:15:44 - multi_handler_app - ERROR - Error message (console + file)
Enter fullscreen mode

Exit fullscreen mode

Each handler works independently, this flexibility lets you design logging setups tailored to your app’s needs.



Formatters

Formatters define how your log messages look. They control the layout and content of each message, so you can include timestamps, log levels, module names, or anything else you find helpful.

Here’s an example with a custom format that includes timestamps and line numbers:

import logging

# Create a logger
logger = logging.getLogger("custom_format_app")
logger.setLevel(logging.DEBUG)

# Create a console handler
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.DEBUG)

# Create a custom formatter with date format
formatter = logging.Formatter(
    "%(asctime)s | %(levelname)s | %(filename)s:%(lineno)d | %(message)s",
    "%Y-%m-%d %H:%M:%S"
)

# Attach formatter to handler
console_handler.setFormatter(formatter)

# Add handler to logger
logger.addHandler(console_handler)

# Example log messages
logger.debug("Debug message with custom format")
logger.info("Info message with custom format")
logger.warning("Warning message with custom format")
logger.error("Error message with custom format")
Enter fullscreen mode

Exit fullscreen mode

Example output:

2025-11-04 09:07:09 | DEBUG | example09.py:24 | Debug message with custom format
2025-11-04 09:07:09 | INFO | example09.py:25 | Info message with custom format
2025-11-04 09:07:09 | WARNING | example09.py:26 | Warning message with custom format
2025-11-04 09:07:09 | ERROR | example09.py:27 | Error message with custom format
Enter fullscreen mode

Exit fullscreen mode

Using consistent formatting makes your logs far more readable and helps tools like ELK Stack, Loki, or Datadog parse them more effectively.



Filters

Filters give you even more control by letting you decide which log records get processed by a handler or logger. You can use them to:

  • Exclude certain messages (e.g., filter out debug logs from third-party libraries).
  • Include only logs from a specific module or containing specific text.

Here’s a simple example of a custom filter that only allows messages containing the word "user":

class UserFilter(logging.Filter):
    def filter(self, record):
        return "user" in record.getMessage().lower()

logger = logging.getLogger("filter_example")
logger.setLevel(logging.DEBUG)

console_handler = logging.StreamHandler()
console_handler.addFilter(UserFilter())

formatter = logging.Formatter("%(levelname)s - %(message)s")
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)

logger.info("User logged in")
logger.info("System update started")
Enter fullscreen mode

Exit fullscreen mode

Output:

INFO - User logged in
Enter fullscreen mode

Exit fullscreen mode

The second message is ignored because it doesn’t contain the word “user.”

Handlers decide where your logs go, Formatters decide how they look, Filters decide which ones get through.

Together, these three components form the backbone of a powerful, flexible, and professional logging setup in Python.




Logging in Real-World Applications

Once your project grows beyond a single file, logging becomes more than just helpful, it becomes essential. You need a consistent, centralized way to capture logs from multiple modules, handle errors gracefully, and manage configurations in a maintainable way.

Let’s look at how to set up logging properly for real-world applications.



Logging in Modules

When working on multi-file projects, you should create a separate logger for each module. The convention is to name your logger after the module using __name__. This makes it easy to identify where each log message originated.

Example project structure:

my_app/
├── __init__.py
├── main.py
└── database.py
Enter fullscreen mode

Exit fullscreen mode

In database.py:

import logging

logger = logging.getLogger(__name__)

def connect():
    logger.info("Connecting to the database...")
    # Simulated failure
    raise ConnectionError("Database not reachable")
Enter fullscreen mode

Exit fullscreen mode

In main.py:

import logging
import database

# Global logging configuration
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)

try:
    database.connect()
except Exception as e:
    logging.error("Error during startup: %s", e)
Enter fullscreen mode

Exit fullscreen mode

Output:

2025-11-03 14:10:25 - database - INFO - Connecting to the database...
2025-11-03 14:10:25 - root - ERROR - Error during startup: Database not reachable
Enter fullscreen mode

Exit fullscreen mode

Each module has its own logger (my_app.database), but they share the same configuration.

This structure makes it easy to trace logs back to the exact source while maintaining consistency across the entire application.



Logging Exceptions

One of the best features of Python’s logging module is its built-in support for exception logging.

When an error occurs, you can use logging.exception() to automatically capture the stack trace along with your message.

import logging

logging.basicConfig(level=logging.ERROR, format="%(asctime)s - %(levelname)s - %(message)s")

try:
    1 / 0
except ZeroDivisionError:
    logging.exception("An error occurred")
Enter fullscreen mode

Exit fullscreen mode

Output:

2025-11-04 09:12:07,889 - ERROR - An error occurred
Traceback (most recent call last):
  File "d:\GitHub\Logging-Article\example11.py", line 6, in 
    1 / 0
    ~~^~~
ZeroDivisionError: division by zero
Enter fullscreen mode

Exit fullscreen mode

The logging.exception() method automatically logs at the ERROR level and includes the full traceback, making it perfect for debugging unexpected crashes or failed operations in production.



Logging with configparser or YAML

As your logging setup grows more complex (multiple handlers, formatters, and modules), hardcoding everything in Python becomes messy.

Instead, you can store your configuration in a file and load it at runtime. This allows you to adjust logging behavior without touching the source code.



Option 1: Using logging.config.fileConfig()

fileConfig() reads from an INI-style configuration file:

logging.ini

[loggers]
keys=root,app

[handlers]
keys=consoleHandler,fileHandler

[formatters]
keys=standardFormatter

[logger_root]
level=WARNING
handlers=consoleHandler

[logger_app]
level=INFO
handlers=consoleHandler,fileHandler
qualname=my_app

[handler_consoleHandler]
class=StreamHandler
level=INFO
formatter=standardFormatter
args=(sys.stdout,)

[handler_fileHandler]
class=FileHandler
level=DEBUG
formatter=standardFormatter
args=("app.log", "a")

[formatter_standardFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
Enter fullscreen mode

Exit fullscreen mode

Load this configuration in Python:

import logging
import logging.config

logging.config.fileConfig("logging.ini")

logger = logging.getLogger("my_app")
logger.info("Application started")
Enter fullscreen mode

Exit fullscreen mode

Returns:

2025-11-04 09:12:58,789 - my_app - INFO - Application started
2025-11-04 09:12:58,789 - my_app - INFO - Application started
Enter fullscreen mode

Exit fullscreen mode



Option 2: Using dictConfig() with YAML

For more readable and modern configurations, YAML is often preferred:

logging.yaml

version: 1
formatters:
  detailed:
    format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
handlers:
  console:
    class: logging.StreamHandler
    level: INFO
    formatter: detailed
    stream: ext://sys.stdout
  file:
    class: logging.FileHandler
    level: DEBUG
    formatter: detailed
    filename: app.log
loggers:
  my_app:
    level: DEBUG
    handlers: [console, file]
    propagate: no
root:
  level: WARNING
  handlers: [console]
Enter fullscreen mode

Exit fullscreen mode

Load it in Python:

import logging.config
import yaml

with open("logging.yaml", "r") as f:
    config = yaml.safe_load(f)
    logging.config.dictConfig(config)

logger = logging.getLogger("my_app")
logger.info("Application initialized")
Enter fullscreen mode

Exit fullscreen mode

Returns:

2025-11-04 09:13:46,456 - my_app - INFO - Application initialized
Enter fullscreen mode

Exit fullscreen mode

Using configuration files like these gives you flexibility and maintainability, you can tweak log levels, add handlers, or change formats without touching your application logic.




Conclusion

Logging might not seem glamorous at first, but once you’ve used it effectively, you’ll wonder how you ever built applications without it. It’s your program’s voice, quietly narrating what’s happening behind the scenes, helping you debug, monitor, and maintain your software with confidence.

The key takeaways from this guide are simple but powerful:

  • Use the logging module, not print(), for structured and manageable output.
  • Configure log levels, handlers, and formatters to control what gets logged, where, and how.
  • Create custom loggers per module to keep large applications organized.
  • Always rotate log files, avoid sensitive data, and include useful context like user IDs or request information.
  • Adjust your log level depending on the environment, detailed in development, focused in production.

The source code for all the examples is at: https://github.com/nunombispo/Logging-Article

The earlier you integrate proper logging into your project, the easier it becomes to scale, troubleshoot, and understand your system as it grows. Logging isn’t just for when things break, it’s an essential part of building reliable software.

Start small, even a simple, well-configured logger can make a world of difference. Over time, you’ll build a logging system that not only records your app’s activity but helps you understand it at a glance.


Follow me on Twitter: https://twitter.com/DevAsService

Follow me on Instagram: https://www.instagram.com/devasservice/

Follow me on TikTok: https://www.tiktok.com/@devasservice

Follow me on YouTube: https://www.youtube.com/@DevAsService





Source link

Leave a Reply

Your email address will not be published. Required fields are marked *