Errors and Exceptions#
What you will learn in this lesson:
Handling runtime errors with
try
andexcept
The addition of
else
andfinally
clausesRaising exceptions
Introduction#
Errors are mistakes that prevent Python from running your code. These can happen before or during execution.
Examples of errors that happen before execution include syntax errors (SyntaxError
) (e.g. forgetting a colon after an if
statement, using an invalid variable name…), and an incorrect indentation (IndentationError
).
# Here we are missing a colon
if True
print("This will cause a SyntaxError")
Cell In[1], line 2
if True
^
SyntaxError: expected ':'
# Here we are missing an indentenation
if True:
print("This will cause a IndentationError")
Cell In[2], line 3
print("This will cause a IndentationError")
^
IndentationError: expected an indented block after 'if' statement on line 2
There also errors that happen during execution. This are usually called runtime errors.
Exceptions are the way of handling this kind of errors.
Some examples of runtime errors include:
NameError
happens when one attempts to use a variable that has not been defined
1 + x*3
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[7], line 1
----> 1 1 + x*3
NameError: name 'x' is not defined
TypeError
happens when operation on an object is not supported.
'2' + 2
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[8], line 1
----> 1 '2' + 2
TypeError: can only concatenate str (not "int") to str
ZeroDivisionError
happens when you try to divide by zero.
1/0
---------------------------------------------------------------------------
ZeroDivisionError Traceback (most recent call last)
Cell In[9], line 1
----> 1 1/0
ZeroDivisionError: division by zero
You can find have a complete list of all errors at https://docs.python.org/3/library/exceptions.html
Handling runtime errors with try
and except
#
We can use try
and except
to chatch runtime errors and specify how to handle these.
The syntax to use both clauses is the following:
try
:
Some code
except
ExceptionType:
Some other code
Let’s see how it with the following example, which includes a list of numbers where the third one has been included as a string. If you tried to do an arithmetic operation with such an element, it should give you an error (a TypeError
specifically) because strings can not perform this kind of operations.
numbers = [1, 2, "3", 4]
for number in numbers:
try:
inverse = 1/number
print(f"1/{number} = {inverse}")
except TypeError:
print(f"The number you tried is of type {type(number)}. Maybe it is invalid with the operation you are trying to do?")
1/1 = 1.0
1/2 = 0.5
The number you tried is of type <class 'str'>. Maybe it is invalid with the operation you are trying to do?
1/4 = 0.25
This is how it works:
The code within the try clause is executed.
If no exception occurs, the block within the except clause is skipped.
As soon as an error is encountered within the
try
block, the execution stops. Now two things may happen:If the error type matches the exception named after the
except
keyword, that part of the code within theexcept
clause is executed.If the error does not match the exception, it is an unhandled exception and execution stops with an error message.
You can also just print the default message of the error handler as follow:
numbers = [1, 2, "3", 4]
for number in numbers:
try:
inverse = 1/number
print(f"1/{number} = {inverse}")
except TypeError as e:
print(f"The number you tried failed because of the following reason:", e)
1/1 = 1.0
1/2 = 0.5
The number you tried failed because of the following reason: unsupported operand type(s) for /: 'int' and 'str'
1/4 = 0.25
Or even do nothing at all, for which you would use the keyword pass
. Normally though, this keyword is used as a placeholder for future code.
numbers = [1, 2, "3", 4]
for number in numbers:
try:
inverse = 1/number
print(f"1/{number} = {inverse}")
except TypeError:
pass
1/1 = 1.0
1/2 = 0.5
1/4 = 0.25
We can also have multiple except
clauses. In this case, we just need to concatenate the exception handlers and the code that we want to execute in that case:
try
:
Some code
except
ExceptionType1:
Some other code
except
ExceptionType2:
More code
except
ExceptionType3:
Even more code
…
numbers = [0, 1, 2, "3", 4]
for number in numbers:
try:
inverse = 1/number
print(f"1/{number} = {inverse}")
except TypeError:
print(f"The number you tried is of type {type(number)}. Maybe it is invalid with the operation you are trying to do?")
except ZeroDivisionError:
print("Ups, you are trying to divide by 0..")
Ups, you are trying to divide by 0..
1/1 = 1.0
1/2 = 0.5
The number you tried is of type <class 'str'>. Maybe it is invalid with the operation you are trying to do?
1/4 = 0.25
We can even execute the same block of code for multiple errors. We just need to include multiple exceptions type as tuples after except
:
numbers = [0, 1, 2, "3", 4]
for number in numbers:
try:
inverse = 1/number
print(f"1/{number} = {inverse}")
except (TypeError, ZeroDivisionError):
print("Mate, check your code, because it seems you have either a type error, or you are trying to divide by 0.")
Mate, check your code, because it seems you have either a type error, or you are trying to divide by 0.
1/1 = 1.0
1/2 = 0.5
Mate, check your code, because it seems you have either a type error, or you are trying to divide by 0.
1/4 = 0.25
Adding finally
and else
blocks#
finally
block runs whether or not an exception occurs, and is useful for cleanup actions. It is optional.
numbers = [0, 1, 2, "3", 4]
for number in numbers:
try:
inverse = 1/number
print(f"1/{number} = {inverse}")
except (TypeError, ZeroDivisionError):
print("Mate, check your code, because it seems you have either a type error, or you are trying to divide by 0.")
finally:
print("This always runs")
Mate, check your code, because it seems you have either a type error, or you are trying to divide by 0.
This always runs
1/1 = 1.0
This always runs
1/2 = 0.5
This always runs
Mate, check your code, because it seems you have either a type error, or you are trying to divide by 0.
This always runs
1/4 = 0.25
This always runs
else
: The else block executes only when no exception occurs.
numbers = [0, 1, 2, "3", 4]
for number in numbers:
try:
inverse = 1/number
#print(f"1/{number} = {inverse}")
except (TypeError, ZeroDivisionError):
print("Mate, check your code, because it seems you have either a type error, or you are trying to divide by 0.")
else:
print("The division was sucessful!!")
Mate, check your code, because it seems you have either a type error, or you are trying to divide by 0.
The division was sucessful!!
The division was sucessful!!
Mate, check your code, because it seems you have either a type error, or you are trying to divide by 0.
The division was sucessful!!
But wait… why not including the print
in else
within the try
block?
numbers = [0, 1, 2, "3", 4]
for number in numbers:
try:
inverse = 1/number
print("The division was sucessful!!")
except (TypeError, ZeroDivisionError):
print("Mate, check your code, because it seems you have either a type error, or you are trying to divide by 0.")
Mate, check your code, because it seems you have either a type error, or you are trying to divide by 0.
The division was sucessful!!
The division was sucessful!!
Mate, check your code, because it seems you have either a type error, or you are trying to divide by 0.
The division was sucessful!!
It works similarly, but if you keep adding code within the try
section you may end up having a hard time to identify which piece caused the exception.
So, using else
clause keeps your code cleaner and easier to read, since it clearly indicates that its block of code will not run if an exception is raised.
Raising exceptions#
Sometimes you may need to specify certain situations in your code to be exceptional and therefore, needed for your attention. In these cases, you can raise an exception yourself with the keyowrd raise
.
age_data = [25, 34, -5, 64, 10]
for ii, age in enumerate(age_data):
try:
if age < 0:
raise ValueError("Age cannot be negative")
print(f"Entry #{ii} has {age} years")
except ValueError as e:
print(e)
Entry #0 has 25 years
Entry #1 has 34 years
Age cannot be negative
Entry #3 has 64 years
Entry #4 has 10 years
Some best practices#
Catch specific exceptions, not all exceptions
try:
result = 10 / 0
except:
print("Something went wrong!") # BAD PRACTICE: too vague
Something went wrong!
try:
result = 10 / 0
except ZeroDivisionError as e:
print(e) # Specific error
division by zero
Provide meaningful messages when raising exceptions.
def meaningful(a, b):
if b == 0:
raise ValueError("The denominator cannot be zero.")
return a / b
try:
divide(10, 0)
except ValueError as e:
print(e) #
The denominator cannot be zero.
Always try to handle exceptions appropriately or log them for debugging. It is better to avoid using the
pass
keyword, unless you are really planning to incorporate future code there
Practice excersises#
Write a function divide_numbers(a, b) that takes two numbers as input and returns their division result. Catch a ZeroDivisionError if the second number is zero, and a TypeError if either input is not a number. Return appropriate error messages for both cases.
# Your answers here
Write a function check_positive_number(num) that:
1 - Tries to check if a number is positive.
2 - If the number is negative, raise a ValueError with the message ‘Number must be positive’.
3 - If no exception occurs, print ‘The number is positive’ inside the else block.
4 - Ensure that, regardless of whether an exception occurs, a final message ‘Check complete’ is printed using a finally block.
# Your answers here