How to define your own exception classes in Python

Hello, Habr!



Your interest in the new book " Python Pro Secrets " has convinced us that the story about Python's quirks is worth continuing. Today we offer to read a small tutorial on creating custom (in the text - your own) exception classes. The author got it interesting, it's hard to disagree with him that the most important advantage of an exception is the completeness and clarity of the error message. Part of the code from the original is in the form of pictures.



Welcome to cat.



Creating your own error classes



Python provides the ability to create your own exception classes. By creating such classes, you can diversify the design of the classes in your application. A custom class of errors could log errors, inspect the object. It is we who define what the exception class does, although usually a custom class can hardly do more than display a message.



Naturally, the type of error itself is important, and we often create our own error types to indicate a specific situation that is usually not covered at the Python level. This way, users of the class will know exactly what is going on when they encounter this error.



This article is divided into two parts. First, we will define an exception class by itself. We will then demonstrate how we can integrate our own exception classes into our Python programs and show how we can thus improve the usability of the classes we design.



MyCustomError custom exception class



Throwing an exception requires the __init__()



and methods __str__()



.




When throwing an exception, we already create an instance of the exception and at the same time display it on the screen. Let's take a closer look at our custom exception class shown below.







The above MyCustomError class has two magic methods, __init__



and __str__



, which are automatically called during exception handling. The method Init



is called when the instance is created, and the method str



is called when the instance is displayed. Therefore, when an exception is thrown, these two methods are usually called immediately after each other. The Python exception throw statement puts a program into an error state.



In the argument list of the method __init__



is *args



. A component *args



is a special pattern matching mode used in functions and methods. It allows you to pass multiple arguments, and stores the passed arguments as a tuple, but at the same time allows you not to pass arguments at all.



In our case, we can say that if MyCustomError



any arguments were passed to the constructor , then we take the first passed argument and assign it to an attribute message



in the object. If no arguments were passed, then the attribute message



will be assigned a value None



.



In the first example, the exception MyCustomError



is thrown without any arguments, so the attribute message



this object is assigned a value None



. A method will be called str



and will display the message 'MyCustomError message has been raised'.







The exception MyCustomError



is thrown without any arguments (parentheses are empty). In other words, such an object design looks non-standard. But this is just syntactic support provided in Python when throwing an exception.



In the second example, it MyCustomError



is passed with a string argument 'We have a problem'. It is set as an attribute of message



the object and displayed as an error message when an exception is thrown.







The code for the MyCustomError exception class is here...



class MyCustomError(Exception):
    def __init__(self, *args):
        if args:
            self.message = args[0]
        else:
            self.message = None

    def __str__(self):
        print('calling str')
        if self.message:
            return 'MyCustomError, {0} '.format(self.message)
        else:
            return 'MyCustomError has been raised'


#  MyCustomError

raise MyCustomError('We have a problem')
      
      





CustomIntFloatDic Class



We create our own dictionary, the values ​​of which can only be integers and floating point numbers.



Let's go ahead and demonstrate how to easily and usefully inject error classes into our own programs. To begin with, I will offer a slightly contrived example. In this fictional example, I'll create my own dictionary that can only accept integers or floating point numbers.



If the user tries to specify any other data type as the value in this dictionary, an exception will be thrown. This exception will provide useful information to the user on how to use this dictionary. In our case, this message directly informs the user that only integers or floating point numbers can be specified as values ​​in this dictionary.



When creating your own dictionary, keep in mind that there are two places where values ​​can be added to the dictionary. Firstly, this can happen in the init method when creating an object (at this stage, the object can already be assigned keys and values), and secondly, when setting keys and values ​​directly in the dictionary. In both of these places, you need to write code to ensure that the value can only be of type int



or float



.



First, I'll define the CustomIntFloatDict class that inherits from the built-in class dict



. dict



is passed in a list of arguments, which are enclosed in parentheses and follow the class name CustomIntFloatDict



.



If a class is instantiated CustomIntFloatDict



, moreover, no arguments are passed to the key and value parameters, they will be set to None



. The expression is if



interpreted as follows: if either the key is equal None



, or the value is equal None



, then a method will be called with the object get_dict()



, which will return the attribute empty_dict



; such an attribute on an object points to an empty list. Remember that class attributes are available on all instances of the class.







The purpose of this class is to allow the user to pass a list or tuple with the keys and values ​​inside. If the user enters a list or a tuple looking for keys and values, then these two enumerated sets will be concatenated using the function zip



the Python language. A hooked variable pointing to an object zip



is iterable, and tuples are unpackable. By iterating over the tuples, I check if val is an instance of the class int



or float



. If it val



doesn't belong to any of these classes, I throw IntFloatValueError



my own exception and pass val as an argument to it.



IntFloatValueError Exception Class



When an exception is thrown, IntFloatValueError



we create an instance of the class IntFloatValueError



and simultaneously display it on the screen. This means that the magic methods init



and will be called str



.



The value that caused the thrown exception is set as an attribute value



accompanying the class IntFloatValueError



. When calling the magic str method, the user receives an error message informing that the value init



in CustomIntFloatDict



is invalid. The user knows what to do to fix this error.







Exception classes IntFloatValueError



and KeyValueConstructError











If no exception is thrown, that is, all val



from the chained object are of types int



or float



, then they will be set using __setitem__()



, and the method from the parent class will do everything for us dict



, as shown below.







KeyValueConstructError Class



What happens if the user enters a type that is not a list or a tuple of keys and values?



Again, this example is a bit artificial, but it is handy to show you how you can use your own exception classes.



If the user does not specify the keys and values ​​as a list or a tuple, an exception will be thrown KeyValueConstructError



. The purpose of this exception is to inform the user that in order to write keys and values ​​to an object CustomIntFloatDict



, a list or a tuple must be specified in the init



class constructor CustomIntFloatDict



.



In the above example, as the second argument to the constructor init



a lot was passed and an exception was thrown because of this KeyValueConstructError



. The benefit of the displayed error message is that the displayed error message informs the user that the keys and values ​​to be inserted must be reported as either a list or a tuple.



Again, when an exception is thrown, a KeyValueConstructError instance is created, and the key and values ​​are passed as arguments to the KeyValueConstructError constructor. They are set as the values ​​of the key and value attributes of KeyValueConstructError and are used in the __str__ method to generate a meaningful error message when the message is displayed on the screen.



Further, I even include the data types inherent in the objects added to the constructor init



- I do this for clarity.







Setting key and value in CustomIntFloatDict



CustomIntFloatDict



inherits from dict



. This means that it will function exactly like a dictionary, except in the places that we choose to selectively change its behavior.



__setitem__



Is a magic method called when setting a key and value in a dictionary. In our implementation, setitem



we check that the value is of type int



or float



, and only after a successful check can it be set in the dictionary. If the check fails, you can use the exception class again IntFloatValueError



. Here you can make sure that if we try to set a string β€˜bad_value’



as a value in the dictionary test_4



, we will get an exception.







All of the code for this tutorial is shown below and posted on Github .



#  ,        int  float  

class IntFloatValueError(Exception):
    def __init__(self, value):
        self.value = value

    def __str__(self):
        return '{} is invalid input, CustomIntFloatDict can only accept ' \
               'integers and floats as its values'.format(self.value)


class KeyValueContructError(Exception):
    def __init__(self, key, value):
        self.key = key
        self.value = value

    def __str__(self):
        return 'keys and values need to be passed as either list or tuple' + '\n' + \
                ' {} is of type: '.format(self.key) + str(type(self.key)) + '\n' + \
                ' {} is of type: '.format(self.value) + str(type(self.value))


class CustomIntFloatDict(dict):

    empty_dict = {}

    def __init__(self, key=None, value=None):

        if key is None or value is None:
            self.get_dict()

        elif not isinstance(key, (tuple, list,)) or not isinstance(value, (tuple, list)):
            raise KeyValueContructError(key, value)
        else:
            zipped = zip(key, value)
            for k, val in zipped:
                if not isinstance(val, (int, float)):
                    raise IntFloatValueError(val)
                dict.__setitem__(self, k, val)

    def get_dict(self):
        return self.empty_dict

    def __setitem__(self, key, value):
        if not isinstance(value, (int, float)):
            raise IntFloatValueError(value)
        return dict.__setitem__(self, key, value)


#  

# test_1 = CustomIntFloatDict()
# print(test_1)
# test_2 = CustomIntFloatDict({'a', 'b'}, [1, 2])
# print(test_2)
# test_3 = CustomIntFloatDict(('x', 'y', 'z'), (10, 'twenty', 30))
# print(test_3)
# test_4 = CustomIntFloatDict(('x', 'y', 'z'), (10, 20, 30))
# print(test_4)
# test_4['r'] = 1.3
# print(test_4)
# test_4['key'] = 'bad_value'
      
      





Conclusion



If you create your own exceptions, then working with the class becomes much more convenient. The exception class should have magic methods init



and str



that are automatically called during exception handling. It is entirely up to you what your own exception class will do. Among the methods shown are those that are responsible for inspecting an object and displaying an informative error message on the screen.



Be that as it may, exception classes make it much easier to handle any errors that arise!



All Articles