Fundamentals 12 min read

Master Python Exception Handling and Logging: Practical Guide with Code

This article explains why robust exception handling and logging are essential in Python development, introduces built‑in exception types, demonstrates try‑except‑else‑finally patterns, shows how to use assert and raise, creates custom exceptions, and provides practical logging examples with code snippets.

Code Mala Tang
Code Mala Tang
Code Mala Tang
Master Python Exception Handling and Logging: Practical Guide with Code

Nobody can write flawless code on the first try, even the creator of a programming language. When problems arise, we must review and debug, making proper handling of exceptions, errors, and logs crucial for easier troubleshooting.

Exception Handling

Python provides many built‑in exceptions such as ImportError , SyntaxError , NameError , ZeroDivisionError , IndentationError , AssertionError , AttributeError , RuntimeError , MemoryError , SystemError , etc. All can be found in the Python documentation.

Using try , except , else and finally to handle exceptions

Assume you want to catch errors while converting a string to an integer. The try block fails, and execution moves to the except block.

<code>x = "City"
try:
    print(int(x))
except:
    print("An exception has occurred!")
</code>

You can also handle specific errors, for example catching a division by zero:

<code>try:
    print(1/0)
except ZeroDivisionError:
    print("You cannot divide a value with zero")
except:
    print("Something else went wrong")
</code>

Printing the default message of a built‑in exception is also possible:

<code>try:
    with open('data.csv') as file:
        read_data = file.read()
except FileNotFoundError as file_error:
    print(file_error)
</code>

Try-Except-Else : If the try block succeeds, execution proceeds to the else block; otherwise it jumps to except .

<code>try:
    result = 1/3
    print("hello")
except ZeroDivisionError as err:
    print(err)
else:
    print(f"Your answer is {result}")
</code>

Try-Except-Else-Finally : The finally block always runs.

<code>try:
    result = 1/3
except ZeroDivisionError as error:
    print(error)
    print("Please change 'y' argument to non-zero value")
except:
    print("Something went wrong")
else:
    print(f"Your answer is {result}")
finally:
    print("Program is finished")
</code>

Nested Exceptions

<code>def divide_numbers(x, y):
    try:
        result = x / y
        print(f"Result: {result}")
    except ZeroDivisionError as e1:
        print("Error: Cannot divide by zero.")
        print(f"Original error: {e1}")
    try:
        result = x / "a"
    except TypeError as e2:
        print(f"Nested Error: Division with an invalid type. Original error: {e2}")

divide_numbers(1, 0)
</code>

assert and raise statements

assert checks a condition and raises an AssertionError with an optional message if the condition is false.

<code>def divide(x, y):
    assert y != 0, "Division by zero error!"
    return x / y

print(divide(10, 2))
print(divide(10, 0))
</code>

raise explicitly throws an exception, allowing custom error messages and types.

<code>def validate_age(age):
    if age < 0:
        raise ValueError("Age cannot be negative!")
    return age

print(validate_age(-5))
</code>

Creating Custom Exceptions

Custom exceptions can capture detailed error information such as file name and line number using the sys module.

<code>import sys

class customexception(Exception):
    def __init__(self, error_message, error_details: sys):
        self.error_message = error_message
        _, _, exc_tb = error_details.exc_info()
        self.lineno = exc_tb.tb_lineno
        self.file_name = exc_tb.tb_frame.f_code.co_filename
    def __str__(self):
        return f"Error occured in python script name {self.file_name} line number {self.lineno} error message {self.error_message}"

if __name__ == "__main__":
    try:
        a = 1/0
    except Exception as e:
        print(e)
        raise customexception(e, sys)
</code>

Logging

Logging tracks application behavior, helps identify errors, and aids debugging, monitoring, and auditing. Unlike print() , logging supports levels (DEBUG, INFO, WARNING, ERROR, CRITICAL) and can persist output to files.

4.1 Log Levels

DEBUG : detailed diagnostic information.

INFO : confirms normal operation.

WARNING : indicates a potential issue.

ERROR : signals a problem that prevents part of the code from working.

CRITICAL : severe error causing program termination.

Configure logging with logging.basicConfig to set level, file name, and format.

<code>import logging
logging.basicConfig(level=logging.INFO)
logging.debug("This is a debug message")
logging.info("Application started")
logging.warning("This is a warning")
logging.error("An error occurred")
logging.critical("Critical failure!")
</code>

Log to a file and capture exception tracebacks:

<code>import logging
logging.basicConfig(filename="error.log", level=logging.ERROR)
try:
    1 / 0
except ZeroDivisionError:
    logging.exception("A division by zero error occurred")
</code>

Creating a Simple Custom Logger for a Project

<code>import logging
import os
from datetime import datetime

LOG_FILE = f"{datetime.now().strftime('%m_%d_%Y_%H_%M_%S')}.log"
log_path = os.path.join(os.getcwd(), "logs")
os.makedirs(log_path, exist_ok=True)
LOG_FILEPATH = os.path.join(log_path, LOG_FILE)
logging.basicConfig(level=logging.INFO, filename=LOG_FILEPATH,
                    format="[%(asctime)s] %(lineno)d %(name)s - %(levelname)s - %(message)s")
</code>

Applying Exceptions and Logging in a Project

The following example shows a data ingestion pipeline that uses pandas, custom exceptions, and logging to monitor execution.

<code>import pandas as pd
import numpy as np
from src.exception.exception import customexception
from src.logger.logging import logging
import os, sys
from sklearn.model_selection import train_test_split
from dataclasses import dataclass
from pathlib import Path

@dataclass
class DataIngestionConfig:
    raw_data_path: str = os.path.join("artifacts", "raw.csv")
    train_data_path: str = os.path.join("artifacts", "train.csv")
    test_data_path: str = os.path.join("artifacts", "test.csv")

class DataIngestion:
    def __init__(self):
        self.ingestion_config = DataIngestionConfig()
    def initiate_data_ingestion(self):
        logging.info("data ingestion started..")
        try:
            data = pd.read_csv(r"IPL-Score-Prediction-App-Machine-Learning\data\ipl_data.csv")
            logging.info("reading a df")
            os.makedirs(os.path.dirname(self.ingestion_config.raw_data_path), exist_ok=True)
            data.to_csv(self.ingestion_config.raw_data_path, index=False)
            logging.info("raw data saved in the artifact folder")
            train_data, test_data = train_test_split(data, test_size=0.17, random_state=42)
            logging.info("train test split completed")
            train_data.to_csv(self.ingestion_config.train_data_path, index=False)
            test_data.to_csv(self.ingestion_config.test_data_path, index=False)
            logging.info("data ingestion part completed")
            return (self.ingestion_config.train_data_path, self.ingestion_config.test_data_path)
        except Exception as e:
            logging.info("problem raised in the data ingestion part")
            raise customexception(e, sys)

if __name__ == "__main__":
    obj = DataIngestion()
    obj.initiate_data_ingestion()
</code>
debuggingpythonException Handlingloggingcustom-exception
Code Mala Tang
Written by

Code Mala Tang

Read source code together, write articles together, and enjoy spicy hot pot together.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.