When you start learning a new programming language, you will first fight a lot with the syntax. Before you even can run your program you already get an error that you need to fix. After a while those syntax errors get less and less, and you can focus on creating a working application.
You quickly realise that valid Python syntax is not enough, your code must be able to handle wrong user input, missing resources or network errors. You now entered the endless challenges of creating a robust application and exceptions (and their handling) are an important part of it.
This post is part of my journey to learn Python. You can find the other parts of this series here.
What are exceptions?
I think of exceptions as exceptional cases that cannot be handled in your application. That can be a simple division by 0 or missing permissions to create a file.
1 2 3 4 5 |
def divide(a, b): print(a / b) divide(4, 0) |
If we execute this code, we get an exception and a stack trace, because a division by 0 has no mathematical solution:
1 2 3 4 5 6 |
Traceback (most recent call last): File "D:/Python/exceptions.py", line 5, in <module> divide(4, 0) File "D:/Python/exceptions.py", line 2, in divide print(a / b) ZeroDivisionError: division by zero |
This tells us that on line 5 we call a function that throws a ZeroDivisionError exception on line 2. The last part of a stack trace is often the most important one, so you should always start at the end to find out what went wrong.
Basic exception handling
With the try: statement we can tell python that we want to handle the exceptions for the next block by ourselves. If we rewrite the divide function like this, Python will print out a nice error message instead of a stack trace:
1 2 3 4 5 |
def divide(a, b): try: print(a / b) except Exception: print("something went wrong") |
To check if we get our error message or a stack trace, we can simply call our method again:
1 2 |
>>> divide(4, 0) something went wrong |
What exception was it?
While in most cases we are happy to prevent an exception from crashing our application, we sometimes need to know what exactly went wrong. Wit a little change we can get the raised exception and find out more about the problem:
1 2 3 4 5 |
def divide(a, b): try: print(a / b) except Exception as error: print(f"{type(error)}: {error}") |
If we run our code now, we see that the division by zero is the problem:
1 2 |
>>> divide(4, 0) <class 'ZeroDivisionError'>: division by zero |
If we enter a string, the error changes:
1 2 |
>>> divide(5, 'a') <class 'TypeError'>: unsupported operand type(s) for /: 'int' and 'str' |
Handle the individual exceptions
As you can see in the examples above, the exceptions have different classes. We can now prevent the most common problems and return a useful error message instead of crashing our application:
1 2 3 4 5 6 7 |
def divide(a, b): try: print(a / b) except ZeroDivisionError: print("division by zero is not supported") except TypeError: print("you can only divide numbers") |
This change in our code gives us a much better user experience when something goes wrong:
1 2 3 4 5 6 7 8 |
>>> divide(4, 0) division by zero is not supported >>> divide(5, 'a') you can only divide numbers >>> divide(4, 2) 2.0 |
There is a little catch you need to be aware of: When an exception occurs, the exception handler searches for an except clauses until one is found that matches the exception – including any parent in the inheritance hierarchy. This means, the first match wins. Therefore, if your first except clause is for Exception, it will be used and the more specific exception that you wrote after it will be ignored.
Airbrake.io has a good explanation of the class hierarchy for exceptions in Python.
Clean-up if an exception occurs or not
Sometimes you have code that should always be executed, regardless of whether an exception has occurred or not. This is especially helpful when you need to clean up resources or close files. Code like that goes into a finally clause at the end of your except blocks:
1 2 3 4 5 6 7 8 9 |
def divide(a, b): try: print(a / b) except ZeroDivisionError: print("division by zero is not supported") except TypeError: print("you can only divide numbers") finally: print("runs always") |
Every call to the divide method will now end with the line “runs always”:
1 2 3 4 5 6 7 8 9 10 11 |
>>> divide(4, 0) division by zero is not supported runs always >>> divide(5, 'a') you can only divide numbers runs always >>> divide(4, 2) 2.0 runs always |
Raising exceptions in your code
We can raise an exception by using the raise keyword and then use the exception class that fits our needs:
1 2 3 4 5 |
def work(input): if isinstance(input, str): raise TypeError("cannot work with strings") else: print(input) |
If we call our code with a number, everything works. However, when we use a string as an argument it crashes with an exception:
1 2 3 4 5 6 7 |
>>> work(1) 1 >>> work('A') Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 3, in work TypeError: cannot work with strings |
Creating your own exceptions
We can make our own exceptions by creating a subclass of the Exception class:
1 2 |
class CustomError(Exception): pass |
We can use our CustomError exception like any other exception:
1 2 3 4 |
>>> raise CustomError("my exception") Traceback (most recent call last): File "<stdin>", line 1, in <module> __main__.CustomError: my exception |
If you want to know more about custom exceptions you should read this short article from Programiz.
Conclusion
Exceptions are an important concept of programming and a necessity to create robust code. Luckily for us, they are easy to use in Python.
1 thought on “Python Friday #12: Exceptions”