# User-Defined Exceptions and Logging in Python

When dealing with application-specific fault scenarios, [Python](https://guicommits.com/handling-exceptions-in-python-like-a-pro/) not only provides a comprehensive collection of built-in [exceptions](https://www.pythontutorial.net/python-oop/python-exceptions/) but also allows you to define your custom [exceptions](https://www.pythontutorial.net/python-oop/python-exception-handling/). Logging [exceptions](https://pynative.com/python-exceptions/) is also a good practice to guarantee effective error handling and debugging. In this post, we'll look at both user-defined [exceptions](https://www.toppr.com/guides/python-guide/tutorials/python-files/python-errors-and-built-in-exceptions/) and how to efficiently report [exceptions](https://studyglance.in/python/exceptions.php) in Python.

## **User-Defined** [**Exceptions**](https://docs.python.org/3/tutorial/errors.html)

* **Define a custom** [**exception**](https://www.geeksforgeeks.org/python-exception-handling/) **class**
    
* **Raise the custom** [**exception**](https://docs.python.org/3/library/exceptions.html)
    
* **Handle the custom** [**exception**](https://www.programiz.com/python-programming/exceptions)
    

### Define a custom exception class

User-defined [exceptions](https://en.wikibooks.org/wiki/Python_Programming/Exceptions) are custom [exception](https://www.w3schools.com/python/python_try_except.asp) classes created by inheriting from Python's basic '[**Exception**](https://www.tutorialspoint.com/python/python_exceptions.htm)' class or any of its [subclasses](https://pythontutorials.eu/basic/exceptions/). These custom [exceptions](https://realpython.com/python-exceptions/) assist you in dealing with error scenarios that are peculiar to your application.

```python
class MyCustomException(Exception):
    def __init__(self, message):
        super().__init__(message)
```

### Raise the custom [exception](https://www.w3schools.com/python/python_ref_exceptions.asp)

When you encounter an error condition in your code, you can raise your custom [exception](https://www.dataquest.io/blog/python-exceptions/) using the `raise` keyword. You can include a [custom error](https://book.pythontips.com/en/latest/exceptions.html) message to provide information about the [exception](https://in.indeed.com/career-advice/career-development/handling-exceptions-in-python)

```python
def some_function(x):
    if x < 0:
        raise MyCustomException("Input should be a positive number.")
    return x * 2
```

### Handle the [custom exception](https://jerrynsh.com/stop-using-exceptions-like-this-in-python/)

A try and [except block](https://www.datacamp.com/tutorial/exception-handling-python) may be used to handle the custom [exception](https://www.scaler.com/topics/python/exception-handling-in-python/). In this block, you can catch the specified [exception](https://www.freecodecamp.org/news/python-print-exception-how-to-try-except-print-an-error/) type and take the following actions

```python
try:
    result = some_function(-5)
except MyCustomException as e:
    print(f"Caught an exception: {e}")
else:
    print(f"Result: {result}")
```

If **some\_function(-5)** is called, it raises a **MyCustomException**, executing the code inside the [except block](https://runestone.academy/ns/books/published/fopp/Exceptions/standard-exceptions.html) and printing the [error message](https://zetcode.com/lang/python/exceptions/), while **some\_function(10)** does not raise an exception.

### Example 1: Managing bank accounts

Building a library for managing bank accounts ensures balances are never negative, raising a [custom exception](https://ncert.nic.in/textbook/pdf/lecs101.pdf) called [**InsufficientFundsException**](https://www.tutorialsteacher.com/python/exception-handling-in-python) if withdrawals would result in a negative balance.

```python
class InsufficientFundsException(Exception):
    def __init__(self, account_number, balance, amount):
        self.account_number = account_number
        self.balance = balance
        self.amount = amount
        super().__init__(f"Account {account_number} has insufficient funds. "
                         f"Balance: {balance}, Withdrawal amount: {amount}")


class BankAccount:
    def __init__(self, account_number, initial_balance):
        self.account_number = account_number
        self.balance = initial_balance

    def withdraw(self, amount):
        if amount <= 0:
            raise ValueError("Withdrawal amount must be greater than 0.")
        if self.balance < amount:
            raise InsufficientFundsException(self.account_number, self.balance, amount)
        self.balance -= amount

# Example usage
account1 = BankAccount("12345", 1000)

try:
    account1.withdraw(1500)  # This will raise InsufficientFundsException
except InsufficientFundsException as e:
    print(e)

try:
    account1.withdraw(500)  # This will succeed
except InsufficientFundsException as e:
    print(e)
else:
    print(f"Withdrawal successful. New balance: {account1.balance}")
```

The **BankAccount** class raises a [custom exception](https://rollbar.com/blog/throwing-exceptions-in-python/) [**InsufficientFundsException**](https://www.edureka.co/blog/exceptions-in-python/) when a withdrawal results in insufficient funds, providing informative error messages and allowing for handling the [exception](https://www.coursera.org/tutorials/python-exception) in a try and [except block](https://www.oreilly.com/library/view/python-standard-library/0596000960/ch01s03.html).

## Logging [Exceptions](https://www.knowledgehut.com/blog/programming/python-exception-handling)

[Exception](https://pythonbasics.org/try-except/) logging is an essential technique in software development. [Logging](https://learnpython.com/blog/python-exceptions/) allows you to capture information about errors that occur in your program, which is useful for troubleshooting and debugging.

* **Import the** [**logging module**](https://odsc.medium.com/handling-exceptions-in-python-object-oriented-programming-a3ab19dfda79)
    
* **Configure the** [**logging settings**](https://python-course.eu/python-tutorial/errors-and-exception-handling.php) **(optional)**
    
* **Wrap your code in a** [**try-except block**](https://intellipaat.com/blog/tutorial/python-tutorial/exception-handling-in-python/)
    
* **Add** [**log messages**](https://data-flair.training/blogs/python-exception/)
    
* **Use different log levels**
    
* **Handle** **exceptions** **as needed**
    

### Import the logging module

Begin by importing the '**logging**' module, which contains [logging](http://www.btechsmartclass.com/python/Python_Tutorial_Python_Exception_handling.html) methods and classes in [Python](https://levelup.gitconnected.com/python-exception-handling-best-practices-and-common-pitfalls-a689c1131a92).

```python
import logging
```

### **Configure the** [**logging settings**](https://www.askpython.com/python/examples/handling-ioerrors) **(optional)**

You can tailor the [logging settings](https://www.makeuseof.com/handle-exceptions-python/) to your requirements. You can, for example, change the log level, select a log file, or specify the log type.

```python
logging.basicConfig(
    filename='app.log',  # Specify a log file
    level=logging.ERROR,  # Set the log level to ERROR or higher
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
```

This option will report only error-level and above messages to the app.log file in the format provided.

### **Wrap your code in a** [**try-except block**](https://blog.airbrake.io/blog/python/class-hierarchy)

Use a '**try**' and '[**except**](https://towardsdatascience.com/python-exceptions-what-why-and-how-44661cad3cd4)' block to surround the code that you wish to monitor for errors. Log the [exception](https://www.techbeamers.com/use-try-except-python/) \[ information in the '[**except**](https://towardsdatascience.com/exception-handling-in-python-from-basic-to-advanced-then-tricks-9b495619730a)' block.

```python
try:
    # Code that may raise an exception
except Exception as e:
    # Log the exception
    logging.error(f"An exception occurred: {str(e)}")
```

If you wish to treat distinct errors differently, you may catch a specific [exception](https://portingguide.readthedocs.io/en/latest/exceptions.html) type, such as except `'MyCustomException as e:'`.

### **Add** [**log messages**](https://www.w3resource.com/python-exercises/python-exception-handling-exercises.php)

You can report extra information about the [exception](https://www.section.io/engineering-education/exceptions-and-error/), such as the stack trace or any relevant context, inside the unless block.

```python
try:
    # Code that may raise an exception
except Exception as e:
    # Log the exception and additional information
    logging.error(f"An exception occurred: {str(e)}", exc_info=True)
```

The **exc\_info=True** parameter will provide the stack trace of the [exception](https://hands-on.cloud/python-exceptions/) in the log.

### **Use different log levels**

Depending on the severity of the error, you can utilize different log levels (**DEBUG, INFO, WARNING, ERROR, CRITICAL)**. For example, for significant mistakes, use **logging.error()** and **logging.warning()** for less serious concerns.

```python
import logging

# Configure the logging settings
logging.basicConfig(
    filename='app.log',
    level=logging.DEBUG,  # Set the lowest log level to DEBUG
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)

class CustomException(Exception):
    pass

def perform_operation(value):
    try:
        result = 10 / value  # Example operation that may raise an exception
        logging.debug("Operation succeeded: %s", result)
    except ZeroDivisionError:
        logging.warning("Division by zero occurred.")
        result = None  # Provide a fallback value or behavior
    except Exception as e:
        logging.error("An unexpected error occurred: %s", str(e))
        raise

    return result

def higher_level_function():
    try:
        result = perform_operation(0)
        if result is None:
            logging.warning("Fallback behavior: Unable to perform the operation.")
        else:
            logging.info("Result of the operation: %s", result)
    except CustomException as e:
        logging.error("Custom exception handled: %s", str(e))
    except Exception as e:
        logging.critical("An unexpected error occurred in higher_level_function: %s", str(e))

if __name__ == "__main__":
    try:
        higher_level_function()
    except Exception as e:
        logging.critical("An error occurred in the main application: %s", str(e))
```

### **Handle** **exceptions** **as needed**

Depending on the needs of your application, you may opt to handle errors graciously (for example, by offering a fallback behavior) or propagate them up the call stack.

```python
import logging

class CustomException(Exception):
    pass

def perform_operation(value):
    try:
        result = 10 / value  # Example operation that may raise an exception
    except ZeroDivisionError:
        logging.error("Division by zero occurred.")
        result = None  # Provide a fallback value or behavior
    except Exception as e:
        logging.error(f"An unexpected error occurred: {str(e)}")
        raise  # Propagate the exception up the call stack for higher-level code to handle

    return result

def higher_level_function():
    try:
        result = perform_operation(0)  # Call the function that may raise an exception
        if result is None:
            print("Fallback behavior: Unable to perform the operation.")
        else:
            print(f"Result of the operation: {result}")
    except CustomException as e:
        print(f"Custom exception handled: {str(e)}")
    except Exception as e:
        print(f"An unexpected error occurred in higher_level_function: {str(e)}")

if __name__ == "__main__":
    try:
        higher_level_function()  # Call the higher-level function
    except Exception as e:
        logging.error(f"An error occurred in the main application: {str(e)}")
```

### The assert Statement

The `assert` statement in Python checks if a condition is True, raising an `AssertionError` [exception](https://stackify.com/how-to-catch-all-exceptions-in-python/) if False, used for debugging during development and testing, but not for production [error handling](https://python101.pythonlibrary.org/chapter7_exception_handling.html).

```python
assert condition, message
```

* `condition` is the expression to check.
    
* `message` (optional) provides additional error information.
    

```python
def divide(a, b):
    assert b != 0, "Division by zero is not allowed"
    return a / b

result = divide(10, 2)  # No error
result = divide(10, 0)  # AssertionError with the specified message
```

<details data-node-type="hn-details-summary"><summary>Summary</summary><div data-type="detailsContent">User-defined <a target="_blank" rel="noopener noreferrer nofollow" href="https://python.land/deep-dives/python-try-except" style="pointer-events: none">exceptions</a> allow you to gracefully handle application-specific mistakes while recording <a target="_blank" rel="noopener noreferrer nofollow" href="https://stackdiary.com/tutorials/python-custom-exception/" style="pointer-events: none">exceptions</a> with <a target="_blank" rel="noopener noreferrer nofollow" href="https://www.studytonight.com/python/exception-handling-python" style="pointer-events: none">Python's</a> logging module allows you to collect fault data for debugging and maintenance. Combining these strategies strengthens and maintains the resilience and maintainability of your <a target="_blank" rel="noopener noreferrer nofollow" href="https://code.tutsplus.com/how-to-handle-exceptions-in-python--cms-28621t" style="pointer-events: none">Python</a> programs.</div></details>
