Back to Blog
Lesson 35 of the Python: Programming from Zero with Python course
PythonAugust 22, 20263 min read

Advanced Error Handling: Custom Exceptions and Logging in Python

Stop relying on print() for debugging. Learn to implement custom exception classes and robust logging to build professional, failure-resistant Python code.

pythonloggingdebuggingerror-handlingbest-practices
Detailed view of programming code in a dark theme on a computer screen.

Previously in this course, we covered Exception Handling basics using try and except blocks. While those tools are essential for preventing crashes, professional software requires more than just stopping a script from exiting—it requires observability and clear failure definitions.

In this lesson, we are moving from basic error suppression to building robust code that tells you exactly what went wrong, where, and why, without leaving a trail of print() statements in your codebase.

Why You Need Structured Logging

When you use print() to debug, you are creating "temporary" logs that are hard to turn off, format, or send to a file. The Python logging module provides a standard way to track events. It allows you to categorize messages by severity levels: DEBUG, INFO, WARNING, ERROR, and CRITICAL.

Instead of cluttering your console, you can configure the logger to output to a file, include timestamps, and even format the output for easier analysis.

PYTHON
import logging

# Configure basic logging to a file
logging.basicConfig(
    filename=CE9178">'app.log',
    level=logging.INFO,
    format=CE9178">'%(asctime)s - %(levelname)s - %(message)s'
)

def process_data(data):
    if not data:
        logging.warning("Received empty data payload.")
        return
    logging.info(f"Processing {len(data)} items.")
    # ... logic here

Defining Custom Exception Classes

Standard exceptions like ValueError or TypeError are helpful, but they don't explain the context of your specific business logic. By defining your own exception classes, you can create a hierarchy that makes your error handling more granular and readable.

A custom exception is simply a class that inherits from Python's built-in Exception class.

PYTHON
class DataProcessingError(Exception):
    CE9178">"""Base class for exceptions in this project."""
    pass

class InvalidDataFormat(DataProcessingError):
    CE9178">"""Raised when the input data structure is incorrect."""
    pass

def validate_payload(payload):
    if "id" not in payload:
        raise InvalidDataFormat("The payload is missing the required CE9178">'id' field.")

By separating your errors into specific classes, you can catch them selectively. This allows you to handle an InvalidDataFormat differently than a ConnectionError (which you might want to retry later).

Handling Complex Failure States

In our ongoing project, we want to ensure that if a data fetch fails, the application doesn't just crash. We need to log the event, provide a meaningful error, and potentially return a safe default value.

Here is how you combine custom exceptions and logging:

PYTHON
import logging

logging.basicConfig(level=logging.ERROR)

class APIDataError(Exception):
    pass

def fetch_user_data(user_id):
    try:
        # Simulate an API call that fails
        raise ConnectionError("Service unreachable")
    except ConnectionError as e:
        logging.error(f"Failed to fetch user {user_id}: {e}")
        raise APIDataError(f"Could not retrieve data for user {user_id}") from e

# Usage
try:
    fetch_user_data(123)
except APIDataError:
    print("User data is temporarily unavailable.")

Hands-on Exercise

  1. Modify your existing CLI project to include a logger.py module.
  2. Define a custom exception called StorageError that is raised when your JSON file fails to save.
  3. Configure the logging module to save all ERROR and CRITICAL level logs to a file named errors.log.
  4. Wrap your file-writing logic in a try...except block that catches StorageError and logs the failure before exiting gracefully.

Common Pitfalls

  • Logging everything at one level: Don't mark every single event as ERROR. Use DEBUG for variable states, INFO for application flow, and ERROR for actual failures.
  • Swallowing Exceptions: Never use an empty except: block. It hides bugs and makes debugging impossible. Always log the exception or re-raise it.
  • Over-complicating hierarchies: You don't need a custom exception for every single line of code. Stick to errors that represent distinct, actionable failure states.

FAQ

Q: Should I use print() for production code? A: No. print() is for quick local scripts. Always use logging for production-grade code to ensure logs can be managed, filtered, and rotated.

Q: Why use raise ... from e? A: This creates an "exception chain," which allows developers to see the original root cause (the ConnectionError) while still catching the custom APIDataError.

Recap

We've moved beyond simple error suppression by implementing the logging module for better observability and creating custom exception classes to define clear, domain-specific failure states. This approach to defensive programming ensures your code remains maintainable and professional as the project grows.

Up next: We will explore how to manage sensitive information safely using environment variables, ensuring your API keys never end up in your source code.

Similar Posts