Exceptions are errors encountered while running a program and they differ from syntax errors, which are errors that the compiler encounters when trying to compile your program.
Let’s give an example:
if 1 == 3):
return TrueOutput:
Cell In[1], line 1
if 1 == 3):
^
SyntaxError: unmatched ')'
Here we have a syntax error and this actually occurre before anything was compiled and by Python’s design it gives a good idea of where the error occurs. Let’s prove that this happens before the program runs:
print("Starting!")
if 1 == 3):
return TrueOutput:
Cell In[2], line 2
if 1 == 3):
^
SyntaxError: unmatched ')'
As we can see, it never got that print() statement because, again, Python tries to compile everything before it actually runs it. This is different than a normal exception in which the program is actually running when the error is encountered. Let’s prove it:
print("Starting!")
a = 5 / 0Output:
Starting!
---------------------------------------------------------------------------
ZeroDivisionError Traceback (most recent call last)
Cell In[3], line 2
1 print("Starting!")
----> 2 a = 5 / 0
ZeroDivisionError: division by zero
Now we got the print() statement.
ZeroDivisionError is one of the built-in exception class that Python provides and it’s a subclass of the main Exception class (even all additional built-in exceptions that we’ll see are):
>>> issubclass(ZeroDivisionError, Exception)
TrueHow to handle Exceptions
We can handle exceptions with try-except block where after the try keyword we put our risky code and then after the except we put the code to run if the exception was catch:
try:
5/0
except:
print("An exception occurred!")Output:
An exception occurred!
In this case we didn’t specify any specific exception to catch. And this is not recommended: we don’s want to have blank execpt statements, but we want to be more specific about what you’re trying to catch. So, let’s modify the example:
try:
5/0
except ZeroDivisionError:
print("You tried to divide by 0!")Output:
You tried to divide by 0!
It’s good to be specific about the types of errors you catch because if an exception is raised deeper within your program or in a dependent library (something you hadn’t anticipated) you want that exception to continue propagating. You should only catch specific errors that you intend to handle. In fact, if we try to cast a string to an integer, that’s not going to be caught by the ZeroDivisionErrorexception:
try:
5/1
int("a")
except ZeroDivisionError:
print("You tried to divide by 0!")Output:
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[7], line 3
1 try:
2 5/1
----> 3 int("a")
4 except ZeroDivisionError:
5 print("You tried to divide by 0!")
ValueError: invalid literal for int() with base 10: 'a'
As we can see, this exception gets raised as a ValueError. If this is something that we want to handle we can modify our except clause to also account for that:
try:
5/1
int("a")
except ValueError as err:
print(f"You didn't give me an integer: {err}")Output:
You didn't give me an integer: invalid literal for int() with base 10: 'a'
Note that the syntax except ValueError as err: allows us to store the ValueError object into the variable err, which means we can do something with it. In our case, we used it to give back a little bit more custom feedback message (that’s the actual message provided within the ValueError).
This is a general pattern code to follow for anything that is a non-trivial exception.
Say we wanted to make a script that acted as a simple calculator. We would like to do some type of continuous loop that will ask for user input do some kind of calculation and then return the result:
print("Enter a space-separated expression: Ex: 5 * 7")
running=True
while running:
to_calc = input("# ")
result = do_calc(to_calc)
print(result)In this code there are few areas where we could potentially run into an issue. The first is going to be receiving user input because, while the script is waiting, the user can decide to hit ctrl+c to stop the script which would raise a KeyboardInterrupt exception:
print("Enter a space-separated expression: Ex: 5 * 7")
running=True
while running:
try:
to_calc = input("# ")
result = do_calc(to_calc)
except KeyboardInterrupt:
print("\nGoodbye!")
running = False
else:
print(result)What else does is it runs an additional statement if an exception is not caught. Now let’s define do_cal function:
def do_calc(calc_expr):
left, op, right = calc_expr.split()
left = int(left)
right = int(right)
if op == "+":
return left + right
elif op == "-":
return left - right
elif op == "*":
return left * right
elif op == "/":
return left / rightOf course this method is pretty naive because if you have an expression like 5* 7 it returns a ValueError:
>>> left, op, right = "5* 7".split()Output:
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[10], line 1
----> 1 left, op, right = "5* 7".split()
ValueError: not enough values to unpack (expected 3, got 2)
So, we can wrap this piece of code into a try-except clause:
class UserError(Exception):
pass
def do_calc(calc_expr):
try:
left, op, right = calc_expr.split()
except ValueError:
raise UserError("Make sure there is one space between each element.")
left = int(left)
right = int(right)
if op == "+":
return left + right
elif op == "-":
return left - right
elif op == "*":
return left * right
elif op == "/":
return left / rightHere we are raising a custom UserError, which means we are creating an exception and passing it up to whataver called us or whatever is above us in the call chain. In our example what’s above us is this piece of code result = do_calc(to_calc). So, we have a new exception and we need to handle it:
print("Enter a space-separated expression: Ex: 5 * 7")
running=True
while running:
try:
to_calc = input("# ")
result = do_calc(to_calc)
except UserError as err:
print(err)
except KeyboardInterrupt:
print("\nGoodbye!")
running = False
else:
print(result)Another potential issue we can run into is the conversion of left and right value to int. What if someone gives a value that can’t be converted into an integer? That’s another kind of exception that we could potentially encounter:
class UserError(Exception):
pass
class UserValueError(UserError):
def __init__(self, bad_val):
self.message = f"'{bad_val}' can't be converted to an integer"
def do_calc(calc_expr):
try:
left, op, right = calc_expr.split()
except ValueError:
raise UserError("Make sure there is one space between each element.")
try:
left = int(left)
except ValueError as err:
raise UserValueError(left)
try:
right = int(right)
except ValueError as err:
raise UserValueError(right)
if op == "+":
return left + right
elif op == "-":
return left - right
elif op == "*":
return left * right
elif op == "/":
return left / rightUntil now we are not using the err variable representing the ValueError. If you want to give a bit more context as to where the error coming from when you’re using custom errors, you can say: raise UserValueError(left) from err.
One last thing to handle in our function is what if someone tries to pass an operation that isn’t plit, minus, multiply, or divide? Let’s fix it:
class UserError(Exception):
pass
class UserValueError(UserError):
def __init__(self, bad_val):
self.message = f"'{bad_val}' can't be converted to an integer."
def do_calc(calc_expr):
try:
left, op, right = calc_expr.split()
except ValueError:
raise UserError("Make sure there is one space between each element.")
try:
left = int(left)
except ValueError as err:
raise UserValueError(left) from err
try:
right = int(right)
except ValueError as err:
raise UserValueError(right) from err
if op == "+":
return left + right
elif op == "-":
return left - right
elif op == "*":
return left * right
elif op == "/":
return left / right
else:
UserError(f"{op} is not a supported operation.")As before, we add this new UserValueError to our main code:
print("Enter a space-separated expression: Ex: 5 * 7")
running=True
while running:
try:
to_calc = input("# ")
result = f"{do_calc(to_calc)}"
except UserError as err:
print(err)
except UserValueError as err:
print(err.message)
except ZeroDivisionError:
print("I'm not willing to divide by 0.")
except KeyboardInterrupt:
print("\nGoodbye!")
running = False
else:
print(result)
""" Potential Output:
1 + 2 -> 3
4 * 6 -> 24
5 / 0 -> I'm not willing to divide by 0.
a + 6 -> a
"""The strange part is that the input a + 6 raised a UserError instead of the expected UserValueError. We anticipated a specific message from UserValueError, but a different behavior occurred. This happened because of how Python handles exceptions: it processes them from top to bottom. Although UserValueError is the more appropriate exception, it inherits from UserError, which appeared first in our code, so UserError was triggered instead.
The rule is: when you have exceptions that inherit from others or are more specific, you should list them before the more general ones. So, let’s adjust the order of the exceptions accordingly:
print("Enter a space-separated expression: Ex: 5 * 7")
running=True
while running:
try:
to_calc = input("# ")
result = f"{do_calc(to_calc)}"
except UserValueError as err:
print(err.message)
except UserError as err:
print(err)
except ZeroDivisionError:
print("I'm not willing to divide by 0.")
except KeyboardInterrupt:
print("\nGoodbye!")
running = False
else:
print(result)
""" Potential Output:
1 + 2 -> 3
4 * 6 -> 24
5 / 0 -> I'm not willing to divide by 0.
a + 6 -> 'a' can't be converted to an integer.
1+2 -> Make sure there is one space between each element.
"""raise syntax
raise is useful when you don’t want to modify the chain where an exception is being called from, so you just want to make sure that you catch the exception possibly do something before you raise the exception back up, but raise will just raise whatever exception it encountered straight back up the call chain:
print("Enter a space-separated expression: Ex: 5 * 7")
running=True
while running:
try:
to_calc = input("# ")
result = f"{do_calc(to_calc)}"
except UserValueError as err:
# do something
raise
except UserError as err:
print(err)
except ZeroDivisionError:
print("I'm not willing to divide by 0.")
except KeyboardInterrupt:
print("\nGoodbye!")
running = False
else:
print(result)
""" Potential Output:
1 + 2 -> 3
4 * 6 -> 24
5 / 0 -> I'm not willing to divide by 0.
a + 6 -> 'a' can't be converted to an integer.
1+2 -> Make sure there is one space between each element.
"""If we give as input a + 6, we got:
/attachments/Pasted-image-20241031092309.png)
finally
Code in finally block is going to run every single time, no matter what happens:
print("Enter a space-separated expression: Ex: 5 * 7")
running=True
while running:
try:
to_calc = input("# ")
result = f"{do_calc(to_calc)}"
except UserValueError as err:
print(err.message)
except UserError as err:
print(err)
except ZeroDivisionError:
print("I'm not willing to divide by 0.")
except KeyboardInterrupt:
print("\nGoodbye!")
running = False
else:
print(result)
finally:
print("Running finally...")
""" Potential Output:
1 + 2 ->
3
Running finally...
4 * 6 ->
24
Running finally...
5 / 0 ->
I'm not willing to divide by 0.
Running finally...
"""