How to Declare Custom Exceptions in Modern Python

Proper way to declare custom exceptions in modern Python?

Maybe I missed the question, but why not:

class MyException(Exception):
pass

To override something (or pass extra args), do this:

class ValidationError(Exception):
def __init__(self, message, errors):
# Call the base class constructor with the parameters it needs
super().__init__(message)

# Now for your custom code...
self.errors = errors

That way you could pass dict of error messages to the second param, and get to it later with e.errors.

In Python 2, you have to use this slightly more complex form of super():

super(ValidationError, self).__init__(message)

When should I declare custom exceptions?

There are two questions merged into one: How often should I use custom exceptions (to not overuse them)? and Should I actually ever prefer custom exceptions (to the builtin ones)? Let's answer both.

Custom exception overusing

The blog post from Dan Bader you linked is a great example of how it should not be done. An example of overusing of custom exceptions. Every exception class should cover a group of related uses (ConfigError, BrowserError, DateParserError). You definitely shouldn't create a new custom exception for every particular situation where something needs to be raised. That's what exception messages are for.

Custom vs. builtin exceptions

This is a more opinion-based topic and it also highly depends on the particular code scenario. I will show two interesting examples (out of possibly many) where I consider using a custom exception can be beneficial.

01: Internals exposure

Let's create a simple web browser module (a thin wrapper around the Requests package):

import requests

def get(url):
return requests.get(url)

Now imagine you want to use your new web browser module in several modules across your package. In some of them you want to catch some possible network related exceptions:

import browser
import requests

try:
browser.get(url)
except requests.RequestException:
pass

The downside of this solution is that you have to import the requests package in every module just to catch an exception. Also you are exposing the internals of the browser module. If you ever decide to change the underlying HTTP library from Requests to something else, you will have to modify all the modules where you were catching the exception. An alternative to catch some general Exception is also discouraged.


If you create a custom exception in your web browser module:

import requests

class RequestException(requests.RequestException):
pass

def get(url):
try:
return requests.get(url)
except requests.RequestException:
raise RequestException

then all your modules will now avoid having the disadvantages described above:

import browser

try:
browser.get(url)
except browser.RequestException:
pass

Notice that this is also exactly the approach used in the Requests package itself - it defines its own RequestException class so you don't have to import the underlying urllib package in your web browser module just to catch the exception it raises.

02: Error shadowing

Custom exceptions are not just about making code more nice. Look at (a slightly modified version of) your code to see something really evil:

def validate(name, value):
if len(name) < int(value):
raise ValueError(f"Name too short: {name}")

return name

Now someone will use your code but instead of propagating your exception in case of a short name he would rather catch it and provide a default name:

name = 'Thomas Jefferson'

try:
username = validate(name, '1O')
except ValueError:
username = 'default user'

The code looks good, doesn't it? Now watch this: If you change the name variable to literally any string, the username variable will always be set to 'default user'. If you defined and raised a custom exception ValidationError, this would not have happened.

raise custom exception in python

You are successfully raising an error. And the try/catch statements sees it, and goes to catch as you have raised an error.

To fully customize errors you can declare them as so:

class CustomError(Exception):
pass
raise CustomError("An error occurred")

results in

__main__.CustomError: An error occurred

How do I define custom exceptions across multiple files?

You just need to qualify customException by calling it as a.customException from modules where you imported a, like so:

a.py:

class customException(Exception):
[irrelevant error handling]

def aFunction():
if [something]:
raise customException

return [stuff]

b.py:

import a

try:
var = a.aFunction()
except a.customException:
var = [something else]

Default message in custom exception - Python

The solution is given by the bellow code:

class CustomException(Exception):
def __init__(self, *args, **kwargs):
default_message = 'This is a default message!'

# if any arguments are passed...
# If you inherit from the exception that takes message as a keyword
# maybe you will need to check kwargs here
if args:
# ... pass them to the super constructor
super().__init__(*args, **kwargs)
else: # else, the exception was raised without arguments ...
# ... pass the default message to the super constructor
super().__init__(default_message, **kwargs)

An equivalent but more succinct solution is:

class CustomException(Exception):
def __init__(self, *args, **kwargs):
default_message = 'This is a default message!'

# if no arguments are passed set the first positional argument
# to be the default message. To do that, we have to replace the
# 'args' tuple with another one, that will only contain the message.
# (we cannot do an assignment since tuples are immutable)
# If you inherit from the exception that takes message as a keyword
# maybe you will need to check kwargs here
if not args: args = (default_message,)

# Call super constructor
super().__init__(*args, **kwargs)

An even more succinct but restricted solution, in a way that you can only raise the CustomException with no arguments is:

class CustomException(Exception):
def __init__(self):
default_message = 'This is a default message!'
super().__init__(default_message)

You can of course save one line, in each of the above solutions, if you just pass the string literal to the constructor rather than using the default_message variable.

If you want the code to be Python 2.7 compatible, then you just replace the: super() with super(CustomException, self).

Now running:

>>> raise CustomException

will output:

Traceback (most recent call last):
File "<stdin>", line 1, in <module>
__main__.CustomException: This is a default message!

and running:

raise CustomException('This is a custom message!')

will output:

Traceback (most recent call last):
File "<stdin>", line 1, in <module>
__main__.CustomException: This is a custom message!

This is the output that the first 2 solutions' code will produce. The last solution, differs in that calling it with at least one argument, like:

raise CustomException('This is a custom message!')

it will output:

Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: __init__() takes 1 positional argument but 2 were given

because it does not permit any arguments to be passed to the CustomException when it is raised.

How to raise my exceptions instead of built-in exceptions?

Add from None when raising your custom Exception:

raise MyExceptions('age should be an integer, not str.') from None

See PEP 409 -- Suppressing exception context for more information.

Passing Errors Types into a Custom Error Handling Class, Python

Firstly, exceptions are instances of the Exception base class, and can be used as exceptions… so in your handler, you could:

def __init__(self, error):
try:
raise error
except FileNotFoundError:
print("Can not reach 'jconfig.json': {}".format(error)
except Exception as e:
print(e)

If you insist on using if statements then maybe you can use isinstance(obj, class) instead of string comparison. So in your case if isinstance(error, FileNotFoundError): ....

Secondly, this is alright if you want to centralize error handling. That really depends on your code. I don’t like this way because control is still in the main function and it is unclear what will happen after the handler finished handling. Will you exit(error_code_bigger_than_zero) or exit(0). Would you raise e to get the Python error trace back for debugging or do you want to end clean?

Thirdly. In Python it is better to create small functions that does one thing and does it good. I don’t think there should be anything in your main except:

if __name__ == “__main__”:
main()

Of course the function main() should be defined somewhere in the same or in another file.



Related Topics



Leave a reply



Submit