DEV Community

Cover image for Mastering Logging in Python
Developer Service
Developer Service

Posted on • Originally published at developer-service.blog

Mastering Logging in Python

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 <module> 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

Top comments (0)