Python - Python Logging: Building Production-Ready Application Logs
Logging is an essential part of developing reliable Python applications. It allows developers to record what an application is doing while it is running. These records can help identify errors, understand application behavior, monitor important events, and troubleshoot problems without stopping the application or adding temporary print() statements.
For small programs, print() statements may be sufficient for basic debugging. However, production applications often generate a large amount of information and need better control over what gets recorded, where the information is stored, and who can access it. Python provides the built-in logging module for this purpose. It supports different severity levels, multiple output destinations, formatting, filtering, and log rotation.
1. What Is Logging in Python?
Logging is the process of recording information about the execution of a program. A log entry can describe a normal application event, a warning, an error, or a serious failure.
For example, an application might record:
2026-08-26 10:15:32 INFO User successfully logged in
2026-08-26 10:16:04 WARNING Login attempt failed
2026-08-26 10:17:21 ERROR Database connection failed
Each entry provides information about what happened and when it happened.
Logging is particularly useful when an application is running on a remote server. Developers may not have direct access to the user's screen or the ability to reproduce a problem immediately. Log files provide a historical record that can be examined later.
2. Logging Versus print()
A common beginner approach is to use print() for debugging:
print("User logged in")
print("Database connection failed")
Although this works for simple programs, it becomes difficult to manage in larger applications.
The logging module provides several advantages:
-
Different severity levels can be assigned to messages.
-
Messages can be written to files.
-
Messages can be sent to the console.
-
Output can have standardized timestamps and formats.
-
Developers can enable or disable certain types of messages.
-
Different modules can have separate loggers.
-
Log files can be rotated automatically.
-
Exceptions can be recorded with useful diagnostic information.
For production software, logging is generally more appropriate than relying on print() statements.
3. Importing the Logging Module
Python's logging functionality is available through the standard library.
import logging
A basic logging example is:
import logging
logging.basicConfig(level=logging.INFO)
logging.info("Application started")
logging.warning("This is a warning")
logging.error("An error occurred")
The basicConfig() function provides a simple way to configure logging for small applications.
4. Python Logging Levels
Logging levels indicate how important a particular event is.
Python provides several standard levels:
| Level | Purpose |
|---|---|
| DEBUG | Detailed information useful for diagnosing problems |
| INFO | Confirmation that normal application operations are working |
| WARNING | Something unexpected happened, but the application can continue |
| ERROR | A serious problem occurred during an operation |
| CRITICAL | A very serious problem that may prevent the application from continuing |
For example:
import logging
logging.basicConfig(level=logging.DEBUG)
logging.debug("Processing request parameters")
logging.info("Application started successfully")
logging.warning("Disk space is getting low")
logging.error("Unable to connect to database")
logging.critical("Application cannot continue")
If the logging level is set to INFO, debug messages will normally be excluded because DEBUG has a lower severity.
For example:
logging.basicConfig(level=logging.INFO)
will display INFO, WARNING, ERROR, and CRITICAL messages.
5. Understanding Loggers
A logger is an object responsible for generating logging messages.
Instead of using the root logging functions throughout a large application, it is better to create named loggers.
import logging
logger = logging.getLogger(__name__)
logger.info("Application started")
__name__ gives the name of the current Python module.
This becomes particularly useful in applications containing multiple files.
For example:
application/
main.py
database.py
authentication.py
payments.py
Each module can have its own logger:
logger = logging.getLogger(__name__)
This makes it easier to determine which part of the application generated a particular message.
6. Configuring Log Output
Logging configuration determines where messages go and how they appear.
A simple configuration can specify the logging level and message format:
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s"
)
logging.info("Server started")
A resulting message may look like:
2026-08-26 10:30:12,345 - INFO - Server started
The format provides useful information for troubleshooting.
Common formatting fields include:
| Field | Meaning |
|---|---|
%(asctime)s |
Date and time of the log event |
%(levelname)s |
Logging level |
%(message)s |
Actual log message |
%(name)s |
Logger name |
%(filename)s |
Source filename |
%(lineno)d |
Source-code line number |
%(funcName)s |
Function that generated the message |
A more detailed format might be:
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s"
)
This can make production troubleshooting much easier.
7. Writing Logs to a File
Console output disappears when the application terminates. For production applications, logs are often stored in files.
Example:
import logging
logging.basicConfig(
filename="application.log",
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s"
)
logging.info("Application started")
logging.warning("Configuration file is missing")
The messages will be written to application.log.
A log file might contain:
2026-08-26 10:30:12,345 - INFO - Application started
2026-08-26 10:31:02,124 - WARNING - Configuration file is missing
This allows developers or administrators to inspect application activity later.
8. Using Handlers
A handler determines where log messages are sent.
Python provides several handlers for different purposes.
Common handlers include:
-
StreamHandlerfor console output -
FileHandlerfor ordinary log files -
RotatingFileHandlerfor size-based log rotation -
TimedRotatingFileHandlerfor time-based log rotation
For example, an application can simultaneously write logs to the console and a file.
import logging
logger = logging.getLogger("application")
logger.setLevel(logging.INFO)
console_handler = logging.StreamHandler()
file_handler = logging.FileHandler("application.log")
logger.addHandler(console_handler)
logger.addHandler(file_handler)
logger.info("Application started")
Here, the logger sends the message to both handlers.
9. Formatting Logs with a Formatter
A formatter determines the structure of each log message.
import logging
logger = logging.getLogger("application")
logger.setLevel(logging.INFO)
handler = logging.StreamHandler()
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.info("Server started")
The formatter ensures that messages have a consistent structure.
This is important because production logs may be processed by monitoring systems or log-analysis tools.
10. Using Filters
Filters provide additional control over which log records are processed.
For example, an application might want a particular handler to receive only messages related to a specific condition.
A filter can be created by subclassing logging.Filter.
import logging
class ApplicationFilter(logging.Filter):
def filter(self, record):
return "application" in record.getMessage()
logger = logging.getLogger("application")
logger.setLevel(logging.INFO)
handler = logging.StreamHandler()
handler.addFilter(ApplicationFilter())
logger.addHandler(handler)
logger.info("application started")
logger.info("database operation completed")
Filters are particularly useful in larger systems where different logs need to be routed to different destinations.
11. Logging Exceptions
One of the most important uses of logging is recording exceptions.
Consider:
import logging
logger = logging.getLogger(__name__)
try:
result = 10 / 0
except ZeroDivisionError:
logger.exception("An error occurred while performing calculation")
logger.exception() is designed to be used inside an exception handler. It records the message along with traceback information.
A traceback helps developers determine where the problem occurred.
For example, instead of simply recording:
ERROR Calculation failed
the log can contain information showing the exception type and the location where the exception occurred.
This makes troubleshooting significantly easier.
12. Why Tracebacks Matter
Suppose a production application crashes because a function receives an unexpected value.
A simple error log might say:
ERROR Processing failed
That does not provide much information.
A traceback can show:
Traceback (most recent call last):
...
ValueError: invalid literal for int()
The developer can then investigate the source of the problem more efficiently.
For this reason, exception logging should normally preserve traceback information rather than recording only a short error message.
13. Logging Sensitive Information
Production logging must be designed carefully.
Developers should avoid logging sensitive information such as:
-
Passwords
-
Authentication tokens
-
Credit card information
-
Private keys
-
Session credentials
-
Personal information that is not required for troubleshooting
For example, this is unsafe:
logger.info("User password: %s", password)
Instead, the application should record only information that is actually necessary:
logger.info("User authentication attempt completed")
Good logging balances diagnostic usefulness with security and privacy requirements.
14. Lazy Message Formatting
Python logging supports deferred formatting.
Instead of:
logger.info(f"User {username} logged in")
it is generally preferable in logging calls to use:
logger.info("User %s logged in", username)
The logging system can then perform formatting only when the message needs to be emitted.
This approach can be beneficial for performance, especially when debug logging is disabled.
15. Log Rotation
A production application can generate large log files over time.
For example, a busy web application might generate hundreds of megabytes or even several gigabytes of logs.
Keeping everything in one file can create storage and maintenance problems.
Python provides RotatingFileHandler for size-based rotation.
import logging
from logging.handlers import RotatingFileHandler
logger = logging.getLogger("application")
logger.setLevel(logging.INFO)
handler = RotatingFileHandler(
"application.log",
maxBytes=5_000_000,
backupCount=3
)
logger.addHandler(handler)
logger.info("Application started")
When the log file reaches the configured size, a new file can be created while older files are retained according to the backup configuration.
16. Time-Based Log Rotation
Logs can also be rotated according to time.
Python provides TimedRotatingFileHandler.
from logging.handlers import TimedRotatingFileHandler
handler = TimedRotatingFileHandler(
"application.log",
when="midnight",
backupCount=7
)
This can create a new log file at regular intervals, such as daily.
Time-based rotation is useful when administrators want separate log files for each day or another defined period.
17. Logging in Multiple Modules
Large Python applications normally contain many modules.
A common approach is to create a logger in each module:
import logging
logger = logging.getLogger(__name__)
def process_order():
logger.info("Order processing started")
Another module can use:
import logging
logger = logging.getLogger(__name__)
def connect_database():
logger.info("Database connection started")
The application can then configure logging centrally.
This approach makes the logging architecture easier to maintain and avoids duplicating configuration throughout the application.
18. Logging Hierarchy
Python logging uses a hierarchy of logger names.
For example:
logging.getLogger("application")
logging.getLogger("application.database")
logging.getLogger("application.authentication")
Here, the database and authentication loggers can be treated as children of the application logger.
This hierarchical structure allows logging configuration to be organized logically.
For example, an application could configure general logging at one level while applying more specific settings to a particular module.
19. Propagation
Log records can move from a child logger to its parent logger through propagation.
For example:
application
application.database
application.authentication
A message generated by application.database can be passed to the parent logger's handlers.
Understanding propagation is important because incorrectly configured handlers can sometimes result in duplicate log messages.
In large applications, developers should understand which logger owns a handler and whether propagation is enabled.
20. Centralized Logging Configuration
In production applications, logging configuration should ideally be centralized rather than scattered throughout the code.
For example, an application might have:
project/
main.py
database.py
authentication.py
logging_config.py
The logging configuration can be defined in one place and imported or initialized when the application starts.
This provides consistency across different modules and makes future configuration changes easier.
Python also supports dictionary-based logging configuration through logging.config.dictConfig().
For example:
import logging.config
config = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"standard": {
"format": "%(asctime)s - %(levelname)s - %(name)s - %(message)s"
}
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "standard"
}
},
"loggers": {
"application": {
"handlers": ["console"],
"level": "INFO"
}
}
}
logging.config.dictConfig(config)
This approach becomes valuable when logging requirements become more complex.
21. Choosing Appropriate Log Levels
Developers should carefully decide which level to use.
Use DEBUG for detailed diagnostic information that is generally unnecessary during normal operation.
Use INFO for significant normal events, such as application startup or successful completion of important operations.
Use WARNING when something unexpected occurs but the application can continue.
Use ERROR when an operation fails or an important problem occurs.
Use CRITICAL for severe failures that may require immediate attention.
For example:
logger.debug("Received request parameters")
logger.info("Order successfully created")
logger.warning("External service response is slow")
logger.error("Order processing failed")
logger.critical("Application startup failed")
Using levels consistently makes logs easier to understand and analyze.
22. Production Logging Best Practices
A good production logging strategy should follow several principles.
First, logs should provide meaningful information rather than recording every minor operation.
Second, messages should be clear and consistent. A message such as:
Database operation failed
is less useful than:
Failed to retrieve customer record from database
Third, timestamps and severity levels should normally be included.
Fourth, exceptions should include useful traceback information.
Fifth, sensitive information should never be exposed unnecessarily.
Sixth, log rotation should be considered for applications that generate significant volumes of logs.
Seventh, different environments can use different logging configurations. Development environments may use DEBUG, while production environments often use a more restrictive level.
23. Example of a Production-Oriented Setup
A basic production-style configuration can combine loggers, handlers, formatters, and file rotation:
import logging
from logging.handlers import RotatingFileHandler
logger = logging.getLogger("application")
logger.setLevel(logging.INFO)
formatter = logging.Formatter(
"%(asctime)s - %(levelname)s - %(name)s - %(message)s"
)
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
file_handler = RotatingFileHandler(
"application.log",
maxBytes=10_000_000,
backupCount=5
)
file_handler.setFormatter(formatter)
logger.addHandler(console_handler)
logger.addHandler(file_handler)
logger.info("Application started")
try:
value = 10 / 0
except ZeroDivisionError:
logger.exception("Calculation failed")
This setup provides both console logging and file logging while limiting the size of individual log files.
24. Common Logging Mistakes
Several mistakes can make application logging less useful.
One common mistake is using print() everywhere instead of a structured logging system.
Another is setting every message to ERROR. If ordinary events are classified as errors, it becomes difficult to identify genuine problems.
Logging excessive information can also make important events difficult to find.
Another problem is recording sensitive information. Logs often have broader access than application databases, so they should be treated as potentially sensitive data.
Finally, applications should avoid creating unnecessary handlers repeatedly. Incorrect configuration can cause duplicate messages and excessive resource usage.
25. Conclusion
Python's logging module provides a flexible foundation for recording and monitoring application activity. It goes far beyond simple print() statements by supporting severity levels, named loggers, handlers, formatters, filters, exception tracebacks, and log rotation.
For small programs, basic logging configuration may be sufficient. For larger applications, developers should create module-specific loggers, centralize configuration, choose appropriate logging levels, protect sensitive information, and manage log-file growth through rotation.
A well-designed logging system makes an application easier to debug, monitor, maintain, and troubleshoot, especially when it is running in a production environment where direct access to the application's execution process may not be available.