Jacob Padilla

Python Custom Exceptions: How to Create and Organize Them

Custom exceptions are a great way to enhance your projects and make it easy for others to work with your code! In this article, I delve into how I make and organize custom exceptions for my projects.

Why Make Custom Exceptions?

Python has many built-in exceptions, such as ValueError, TypeError, IndexError, and about 64 others, but they are all very general. Many times, it can be much better to make tailored exceptions for specific scenarios in your projects. For example, let's say you're building a social media bot that posts tweets on Twitter/X for you. There's no built-in exception that can accurately describe an error such as not being able to click on the β€œPost Tweet” button or an error involving the Twitter website being down.

Creating a Basic Custom Exception

To make a basic custom exception, all we really want to do is make a different name for the β€œdefault” exception that accurately describes a potential error in a project.

To do this, all we need to do is make a class with a descriptive name, inherit from the Exception class and either add a pass statement or a docstring explaining the exception. By adding no code in our custom exception, we can let the built-in Exception class handle all of the functionality, which means we can raise and catch our custom exception like any other built-in one!

class CustomError(Exception):
Β  Β  """ Some documentation about what this custom exception is for. """

Raising & Catching Custom Exceptions

Because we’re inheriting from the Exception class, we can raise and catch our custom exception like any standard one. Just ensure it’s imported into the file you’re using!

try:
 Β  Β raise CustomError('Message goes here')
except CustomError as e:
 Β  Β print(f'Error: {e}')

Output:

Error: Message goes here

Add a Default Message to Your Exceptions

When raising built-in exceptions in Python, you can add a message to your exception, but who really remembers to add a unique message every time?

When making custom exceptions, you can add all sorts of custom variables, including a default exception message! In the example below, if the user of the custom exception doesn’t add a unique message, the default message is used instead.

class CustomError(Exception):
Β  Β  default_message = 'A custom exception for ___ class'
 Β  Β 
Β  Β  def __init__(self, message = None):
 Β  Β  Β  Β super().__init__(message or default_message)

Exception Class Hierarchy

In Python, exceptions are structured using class hierarchies. When creating custom exceptions, it's crucial to inherit from the appropriate exception class to maintain clarity and correctness.

Below is a list of all the built-in Python exceptions and their hierarchies, from https://docs.python.org/3/library/exceptions.html.

BaseException
β”œβ”€β”€ BaseExceptionGroup
β”œβ”€β”€ GeneratorExit
β”œβ”€β”€ KeyboardInterrupt
β”œβ”€β”€ SystemExit
└── Exception
β”œβ”€β”€ ArithmeticError
β”‚ β”œβ”€β”€ FloatingPointError
β”‚ β”œβ”€β”€ OverflowError
β”‚ └── ZeroDivisionError
β”œβ”€β”€ AssertionError
β”œβ”€β”€ AttributeError
β”œβ”€β”€ BufferError
β”œβ”€β”€ EOFError
β”œβ”€β”€ ExceptionGroup [BaseExceptionGroup]
β”œβ”€β”€ ImportError
β”‚ └── ModuleNotFoundError
β”œβ”€β”€ LookupError
β”‚ β”œβ”€β”€ IndexError
β”‚ └── KeyError
β”œβ”€β”€ MemoryError
β”œβ”€β”€ NameError
β”‚ └── UnboundLocalError
β”œβ”€β”€ OSError
β”‚ β”œβ”€β”€ BlockingIOError
β”‚ β”œβ”€β”€ ChildProcessError
β”‚ β”œβ”€β”€ ConnectionError
β”‚ β”‚ β”œβ”€β”€ BrokenPipeError
β”‚ β”‚ β”œβ”€β”€ ConnectionAbortedError
β”‚ β”‚ β”œβ”€β”€ ConnectionRefusedError
β”‚ β”‚ └── ConnectionResetError
β”‚ β”œβ”€β”€ FileExistsError
β”‚ β”œβ”€β”€ FileNotFoundError
β”‚ β”œβ”€β”€ InterruptedError
β”‚ β”œβ”€β”€ IsADirectoryError
β”‚ β”œβ”€β”€ NotADirectoryError
β”‚ β”œβ”€β”€ PermissionError
β”‚ β”œβ”€β”€ ProcessLookupError
β”‚ └── TimeoutError
β”œβ”€β”€ ReferenceError
β”œβ”€β”€ RuntimeError
β”‚ β”œβ”€β”€ NotImplementedError
β”‚ └── RecursionError
β”œβ”€β”€ StopAsyncIteration
β”œβ”€β”€ StopIteration
β”œβ”€β”€ SyntaxError
β”‚ └── IndentationError
β”‚ └── TabError
β”œβ”€β”€ SystemError
β”œβ”€β”€ TypeError
β”œβ”€β”€ ValueError
β”‚ └── UnicodeError
β”‚ β”œβ”€β”€ UnicodeDecodeError
β”‚ β”œβ”€β”€ UnicodeEncodeError
β”‚ └── UnicodeTranslateError
└── Warning
β”œβ”€β”€ BytesWarning
β”œβ”€β”€ DeprecationWarning
β”œβ”€β”€ EncodingWarning
β”œβ”€β”€ FutureWarning
β”œβ”€β”€ ImportWarning
β”œβ”€β”€ PendingDeprecationWarning
β”œβ”€β”€ ResourceWarning
β”œβ”€β”€ RuntimeWarning
β”œβ”€β”€ SyntaxWarning
β”œβ”€β”€ UnicodeWarning
└── UserWarning

As you can see from above, all exceptions derive from the BaseException. However, I wouldn't recommend having your custom exceptions directly inherit from the BaseException since KeyboardInterrupt and SystemExit are subclasses of BaseException.

Suppose you inherit from BaseException and use a try-except blocks to catch your custom exception. In that case, Python might inadvertently handle your custom exception when you are actually trying to stop your code using ctrl-c (which raises KeyboardInterrupt) or triggering a SystemExit. This is why it’s usually best to inherit from the Exception class, which is a subclass of BaseException.

For bigger projects with many exceptions, making your own parent exception class can sometimes be a good idea.

For example, let’s say you’re making some sort of bot; you may have 25 different types of exceptions related to clicking on different parts of a website. So logically, it would make sense to first have a WebsiteClickException class that inherits from Exception and then have all of your clicking exceptions inert from WebsiteClickException. The benefit of this is that you, or the people using your code, can choose to either handle specific clicking exceptions in particular ways or have a catch-all handler for every type of clicking exception.

class ClickException(Exception):
 Β  Β """Base exception for all clicking related errors."""
 Β  Β default_message = 'A click-related error occurred.'

 Β  Β def __init__(self, message = None):
 Β  Β  Β  Β super().__init__(message or self.default_message)

class ElementNotFoundException(ClickException):
 Β  Β """Raised when the element to be clicked is not found."""

class ElementNotClickableException(ClickException):
 Β  Β """Raised when the element is present but not clickable."""

class PageNotLoadedException(ClickException):
 Β  Β """Raised when the page hasn't loaded properly."""


# Example usage:
try:
 Β  Β # Simulated logic: Let's assume we couldn't find an element
 Β  Β raise ElementNotFoundException()

except ElementNotClickableException as e:
 Β  Β # Handle ElementNotClickableException differently
 Β  Β print(f"ElementNotClickableException Error: {e}")

except ClickException as e:
 Β  Β # Catch all other click exceptions
 Β  Β print(f"Error: {e}")

Output:

Error: A click-related error occurred.

See how easy it is to catch all β€œclicking” exceptions or just a specific one!?

Organizing Multiple Custom Exceptions

If you’re making one or two custom exceptions for a Python class, it makes sense to put the exception classes in the same file as the Python class that they are made for. However, once you have many custom exceptions, I recommend putting them in a file called exceptions.py to both stay organized and maintain code that is intuitive to import into other modules.

Best Practices & Conclusion

Thanks for reading my article about custom exceptions in Python!

I’ll leave you with a few tips to think about when making your own:

  • Meaningful Naming: Always name your exceptions in a way that they convey their purpose clearly. Typically, names ending in Error or Exception are a good idea.
  • Documentation: Always document your custom exceptions, explaining the scenarios where they might be raised.
  • Avoid Overuse:Β Don't create custom exceptions for every possible error. Only create them when they offer more clarity or handle specific scenarios that built-in exceptions can't cover effectively.

To sum up, custom exceptions offer an enhanced way to represent errors in your Python programs. They not only make your code more readable but also allow for more granular error handling.