On this page
article
Logging in Python
Replace print statements with proper logging — log levels, handlers, formatters, and production logging best practices.
print() is fine for debugging, but production applications need logging — configurable, filterable, and writable to files, databases, or monitoring services.
Basic Logging
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
logger.debug("Detailed debug info")
logger.info("Application started")
logger.warning("Disk space low")
logger.error("Failed to connect to database")
logger.critical("System shutting down")
Output:
INFO:__main__:Application started
WARNING:__main__:Disk space low
Log Levels
| Level | Value | Use Case |
|---|---|---|
| DEBUG | 10 | Detailed diagnostic info |
| INFO | 20 | General operational messages |
| WARNING | 30 | Something unexpected but not fatal |
| ERROR | 40 | A serious problem |
| CRITICAL | 50 | Program may be unable to continue |
Set level to control verbosity — INFO in production, DEBUG in development.
Formatting Output
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
Output:
2024-06-15 10:30:00 [INFO] myapp: Server started on port 8000
Handlers — Multiple Outputs
logger = logging.getLogger("myapp")
logger.setLevel(logging.DEBUG)
# Console handler
console = logging.StreamHandler()
console.setLevel(logging.INFO)
console.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
# File handler
file_handler = logging.FileHandler("app.log")
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(logging.Formatter(
"%(asctime)s [%(levelname)s] %(name)s:%(lineno)d — %(message)s"
))
logger.addHandler(console)
logger.addHandler(file_handler)
Module-Level Loggers
Each module gets its own logger:
# services/api.py
import logging
logger = logging.getLogger(__name__)
def fetch_data(url):
logger.info(f"Fetching {url}")
try:
...
except ConnectionError:
logger.error(f"Connection failed for {url}", exc_info=True)
raise
exc_info=True includes the full traceback.
Logging Exceptions
try:
result = 10 / 0
except ZeroDivisionError:
logger.exception("Calculation failed")
# Same as: logger.error("...", exc_info=True)
Configuration from File
import logging.config
LOGGING_CONFIG = {
"version": 1,
"formatters": {
"standard": {
"format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s",
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "standard",
"level": "INFO",
},
"file": {
"class": "logging.handlers.RotatingFileHandler",
"filename": "app.log",
"maxBytes": 10_000_000,
"backupCount": 5,
"formatter": "standard",
"level": "DEBUG",
},
},
"root": {
"handlers": ["console", "file"],
"level": "DEBUG",
},
}
logging.config.dictConfig(LOGGING_CONFIG)
Structured Logging (JSON)
For log aggregation tools (ELK, Datadog, CloudWatch):
import json
import logging
class JSONFormatter(logging.Formatter):
def format(self, record):
return json.dumps({
"timestamp": self.formatTime(record),
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
})
handler = logging.StreamHandler()
handler.setFormatter(JSONFormatter())
logging.getLogger().addHandler(handler)
Or use python-json-logger package.
Best Practices
- Use
logging, notprint()in application code - One logger per module —
logger = logging.getLogger(__name__) - Log at the right level — don’t log everything as ERROR
- Include context — user ID, request ID, operation name
- Never log secrets — passwords, tokens, API keys
- Use rotating file handlers to prevent disk fill
- Configure via environment — log level from env var
import os
level = os.getenv("LOG_LEVEL", "INFO")
logging.basicConfig(level=getattr(logging, level))
Proper logging is essential for debugging production issues and monitoring application health.