Create your own Python custom exception classes tailored to your application
Proper exception handling gives you easier to read code but also requires you to think very careful what you want to do.
Using exceptions in Python looks easy but it is not. Probably you should study exceptions and exception handling before writing any Python code but TL;DR. There are examples on the internet, unfortunately most are very trivial. Anyway, I researched this and came up with some code I thought I share with you. Leave a comment if you have suggestions.
What is an error and what is an exception, what is the difference? In Python an exception is raised when an error occurs during program execution.The error can for example be a ValueError, ZeroDivisionError.
The advantages of exceptions over status returns can be summarized as follows, see also link below 'Why is it better to throw an exception rather than return an error code?':
- Exceptions leaves your code clean of all the checks necessary when testing status returns on every call
- Exceptions let you use the return value of functions for actual values
- Most importantly: exceptions can't be ignored through inaction, while status returns can
Not all people are that positive, but I am not going to start a war here.
Somewhere in our code we can have an exception handler that can be used e.g. to rollback a database insert. We can also let the exception bubble to the top most code and present an error to the user.
Logging and parameters
If something happens we must make sure that we log the error. To make locating and solving a (coding) problems more easy we we put as much information as possible in our log. I decided I wanted the following information in the log:
- a traceback
- the following (optional) parameters:
- e
This the value (often) returned by an exception - code
This can be our own error code - message
This is the message we want to show to the user (website visitor, API user) - details
More information that can be helpful to the user, e.g. supplied parameters - fargs
These are the arguments of the function where the exception occured, useful for debugging
- e
An example
Assume we create an application that uses a database class and imap server class. We create our custom Exception handler. It is good practice to have it in a file exceptions.py that we import in our application:
# file: exceptions.py
class AppError(Exception):
def __init__(self, e=None, code=None, message=None, details=None, fargs=None):
self.e = e
self.code = code
self.message = message
self.details = details
self.fargs = fargs
def get_e(self):
return self.e
def get_code(self):
return self.code
def get_message(self):
return self.message
def get_details(self):
return self.details
def __str__(self):
s_items = []
if self.e is not None:
s_items.append('e = {}'.format(self.e))
if self.code is not None:
s_items.append('code = {}'.format(self.code))
if self.message is not None:
s_items.append('message = {}'.format(self.message))
if self.details is not None:
s_items.append('details = {}'.format(self.details))
if self.fargs is not None:
s_items.append('fargs = {}'.format(self.fargs))
return ', '.join(s_items)
class DbError(AppError):
pass
class IMAPServerError(AppError):
pass
This is very much like creating normal classes. In this case we pass zero or more named arguments to the exception. We need the __str__() method to let Python return a string with available data that will be stored in the app.log file.If we omit __str__() then app.log only shows: 'exceptions.IMAPServerError'.
The app code:
# file: app.py
from exceptions import AppError, DbError, IMAPServerError
import logging
logging.basicConfig(
format='%(asctime)s - %(levelname)s: %(message)s',
filename='app.log',
level=logging.DEBUG
#level=logging.INFO
)
class A:
def do_a(self, a):
# connect to remote system
b = B()
b.do_b('unnamed parameter', b=a, c={ 'one': 'first', 'two': 'second' })
# do something else
class B:
def do_b(self, a, b=None, c=None):
fname = 'B.do_b'
fargs = locals()
abc = 'not in locals'
# check values
if b not in [8, 16]:
raise IMAPServerError(
fargs = fargs,
e = 'Input must be 8 or 16',
message = 'Input must be 8 or 16, value supplied: {}'.format(b)
)
# connect to remote system
try:
# simulate something went wrong
# raise IMAPServerError('error 123')
if b == 8:
d = b/0
except (ZeroDivisionError, IMAPServerError) as e:
raise IMAPServerError(
fargs = fargs,
e = e,
message = 'Connection to remote system failed. Please check your settings',
details = 'Connection parameters: username = John'
) from e
def run(i):
a = A()
a.do_a(i)
def do_run():
# 7: input error
# 8: connection error
# 16: no error
for i in [7, 8, 16]:
print('Run with i = {}'.format(i))
try:
run(i)
print('No error(s)')
except (DbError, IMAPServerError) as e:
logging.exception('Stack trace')
print('Error: {}'.format(e.message))
print('Details: {}'.format(e.details))
print('Ready')
if __name__ == '__main__':
do_run()
Note that exceptions are logged using 'logging.exception' to a file app.log. Also we use the Python locals() function to capture the arguments of the function or method where the exception occurred. In the 'connect to remote system' exception, we re-raise with all the parameters we want in the log. Finally, we use 'raise ... from e' construct here. This reveals the ZeroDivisionError as the original cause.
To run, type:
python3 app.py
The result is:
Run with i = 7
Error: Input must be 8 or 16, value supplied: 7
Details: None
Run with i = 8
Error: Connection to remote system failed. Please check your settings
Details: Connection parameters: username = John
Run with i = 16
No error(s)
Ready
and the log file app.log:
2020-06-17 15:29:41,939 - ERROR: Stack trace
Traceback (most recent call last):
File "app.py", line 71, in do_run
run(i)
File "app.py", line 59, in run
a.do_a(i)
File "app.py", line 19, in do_a
b.do_b('unnamed parameter', b=a, c={ 'one': 'first', 'two': 'second' })
File "app.py", line 38, in do_b
message = 'Input must be 8 or 16, value supplied: {}'.format(b)
exceptions.IMAPServerError: e = Input must be 8 or 16, message = Input must be 8 or 16, value supplied: 7, fargs = {'fname': 'B.do_b', 'c': {'one': 'first', 'two': 'second'}, 'b': 7, 'a': 'unnamed parameter', 'self': <__main__.B object at 0x7f20aab66748>}
2020-06-17 15:29:41,939 - ERROR: Stack trace
Traceback (most recent call last):
File "app.py", line 46, in do_b
d = b/0
ZeroDivisionError: division by zero
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "app.py", line 71, in do_run
run(i)
File "app.py", line 59, in run
a.do_a(i)
File "app.py", line 19, in do_a
b.do_b('unnamed parameter', b=a, c={ 'one': 'first', 'two': 'second' })
File "app.py", line 53, in do_b
) from e
exceptions.IMAPServerError: e = division by zero, message = Connection to remote system failed. Please check your settings, details = Connection parameters: username = John, fargs = {'fname': 'B.do_b', 'c': {'one': 'first', 'two': 'second'}, 'b': 8, 'a': 'unnamed parameter', 'self': <__main__.B object at 0x7f20aab66748>}
app.log contains two ERROR entries. One for the 'Input must be 8 or 16' error and one for the 'Connection to remote system failed' error.
Summary
Life could be simple but it is not. Proper exception handling gives you easier to read code but also requires you to think very careful what you want to do. It would be nice if we also can add the arguments of the functions called leading to the function raising the exception but this is more difficult.
Links / credits
How to get value of arguments passed to functions on the stack?
https://stackoverflow.com/questions/6061744/how-to-get-value-of-arguments-passed-to-functions-on-the-stack
Proper way to declare custom exceptions in modern Python?
https://stackoverflow.com/questions/1319615/proper-way-to-declare-custom-exceptions-in-modern-python
The Most Diabolical Python Antipattern
https://realpython.com/the-most-diabolical-python-antipattern/
Why Exceptions Suck (ckwop.me.uk)
https://news.ycombinator.com/item?id=232890
Why is it better to throw an exception rather than return an error code?
https://stackoverflow.com/questions/4670987/why-is-it-better-to-throw-an-exception-rather-than-return-an-error-code
Your API sucks (so I’ll catch all exceptions)
https://dorinlazar.ro/your-api-sucks-catch-exceptions/
Read more
Exceptions
Recent
- Hiding database UUID primary keys of your web application
- Don't Repeat Yourself (DRY) with Jinja2
- SQLAlchemy, PostgreSQL, maximum number of rows per user
- Show the values in SQLAlchemy dynamic filters
- Secure data transfer with Public Key encryption and pyNaCl
- rqlite: a high-availability and distributed SQLite alternative
Most viewed
- Using Python's pyOpenSSL to verify SSL certificates downloaded from a host
- Using UUIDs instead of Integer Autoincrement Primary Keys with SQLAlchemy and MariaDb
- Connect to a service on a Docker host from a Docker container
- Using PyInstaller and Cython to create a Python executable
- SQLAlchemy: Using Cascade Deletes to delete related objects
- Flask RESTful API request parameter validation with Marshmallow schemas