Skip to main content

08.2 - Custom Exceptions & the logging Module

Theory 25 min Intermediate

Custom Exceptions

Create custom exceptions by subclassing Exception:

# Base exception for your application
class AppError(Exception):
"""Base exception for all application errors."""
pass


# Specific exceptions
class ValidationError(AppError):
"""Raised when input validation fails."""
def __init__(self, field, value, message):
self.field = field
self.value = value
super().__init__(f"Validation failed for '{field}': {message} (got: {value!r})")


class DatabaseError(AppError):
"""Raised when database operations fail."""
def __init__(self, operation, message):
self.operation = operation
super().__init__(f"Database {operation} failed: {message}")


class NotFoundError(AppError):
"""Raised when a requested resource doesn't exist."""
def __init__(self, resource, identifier):
self.resource = resource
self.identifier = identifier
super().__init__(f"{resource} with id={identifier!r} not found")


# Usage
def get_user(user_id):
if not isinstance(user_id, int):
raise ValidationError("user_id", user_id, "must be an integer")
user = db.find(user_id)
if user is None:
raise NotFoundError("User", user_id)
return user

try:
user = get_user("abc")
except ValidationError as e:
print(f"Invalid input: {e.field} = {e.value}")
except NotFoundError as e:
print(f"Not found: {e.resource} #{e.identifier}")
except AppError as e:
print(f"Application error: {e}")

The logging Module

logging is the professional alternative to print() for debugging and monitoring.

Log Levels

LevelNumericUse
DEBUG10Detailed diagnostics
INFO20Normal operations
WARNING30Potential issues
ERROR40Errors that allow continuing
CRITICAL50Fatal — app cannot continue

Basic Configuration

import logging

# Basic setup (for scripts)
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
)

logger = logging.getLogger(__name__)

logger.debug("Debug info: processing item %d", item_id)
logger.info("User %s logged in", username)
logger.warning("Disk space low: %d%% remaining", pct)
logger.error("Failed to connect to database: %s", error)
logger.critical("System out of memory — shutting down")

Production-Ready Logging Setup

import logging
from logging.handlers import RotatingFileHandler

def setup_logging(log_file="app.log", level=logging.INFO):
"""Configure application-wide logging."""
formatter = logging.Formatter(
fmt="%(asctime)s [%(levelname)-8s] %(name)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
)

# Root logger
root_logger = logging.getLogger()
root_logger.setLevel(level)

# Console handler
console = logging.StreamHandler()
console.setFormatter(formatter)
console.setLevel(logging.INFO)

# File handler (rotates at 5MB, keeps 3 backups)
file_handler = RotatingFileHandler(
log_file, maxBytes=5 * 1024 * 1024, backupCount=3
)
file_handler.setFormatter(formatter)
file_handler.setLevel(logging.DEBUG)

root_logger.addHandler(console)
root_logger.addHandler(file_handler)

# In each module:
logger = logging.getLogger(__name__)

Logging Exceptions

try:
result = risky_operation()
except Exception as e:
# Log with full traceback
logger.exception("Operation failed") # equivalent to logger.error(..., exc_info=True)
# OR
logger.error("Operation failed: %s", e, exc_info=True)

Featureprint()logging
LevelsNoneDEBUG/INFO/WARNING/ERROR/CRITICAL
File output>>sys.stderr onlyFile, rotating, network
TimestampsManualBuilt-in
Module nameManual%(name)s
Production useNoYes
PerformanceSlow (always runs)Fast (filtered by level)

Key Vocabulary

TermDefinition
Custom exceptionA class inheriting from Exception for domain-specific errors
Exception hierarchyOrganizing related exceptions in a class tree
loggingStandard library module for professional application logging
Log levelSeverity of a log message: DEBUG < INFO < WARNING < ERROR < CRITICAL
HandlerDestination for log messages (console, file, network)
FormatterTemplate for log message format
logger.exception()Log an error with full stack trace
RotatingFileHandlerFile handler that rotates when a size limit is reached

Summary

  • Create custom exceptions by subclassing Exception — add fields for structured error data
  • Organize exceptions in a hierarchy: AppErrorValidationError, NotFoundError
  • Replace print() with logging in any code beyond simple scripts
  • Use logging.getLogger(__name__) in every module
  • Configure handlers (console + file) once in your entry point
  • logger.exception() automatically includes the full stack trace