"Exceptions are the gentle reminders that perfection is a journey, not a destination."
- Introduction
- The
try
&except
Block - The
else
Block - The
finally
Block - Raising Exceptions
- Exception Chaining
- Quiz
- Homework
Error handling is an important component of programming that provides opportunity for code to adequately respond to unexpected situations. In Python, errors are managed through the use of exceptions, which are special objects that represent error conditions.
When Python interpreter stumbles upon a situation, it cannot cope with, it raises an exception. If the exception isn't handled, the program will terminate abruptly, which can lead to a poor user experience or even loss of data.
In python, error handling is managed with the use of try
and except
keywords.
The try
keyword indicates a beginning of a block. Python will first try to run the code inside the block and if an error occurs, it will look for the instructions in except
block. The error can be specified and the code inside the block will be executed only in case exception was caught.
try:
result = 10 / 0 # This will raise a ZeroDivisionError
except ZeroDivisionError:
print("Cannot divide by zero!")
Cannot divide by zero!
The code inside the try
block raises a ZeroDivisionError
. The except
block catches this exception and prints a message, without crashing the programm.
For debugging purposes, or sometimes even in production, when you aren't sure about the possible errors or you simply
want to ignore them you can use except Exception as e
construction.
try:
print(3 / 0) # this will raise ZeroDivisionError
except Exception as e:
print(f"An error occurred: {e}")
An error occurred: division by zero
You could also use bare except
block solely for debugging purposes, however it is highly advised against it and simply
forbidden in development. It is not a good idea to use this construction because it may lead to bad overall performance
of the program.
responses = []
for i in range(0, 9999):
try:
response = requests.get("http://example.com")
responses.append(response)
except:
pass
print(len(responses))
6785
The provided code is using requsts
module, you will learn more about it in the next lesson, for now, all you need to
know is that it can be used to retrieve information from the website, in the process called parsing.
You would expect this program to print 9999 as we have tried to access "http://example.com" 9999 and should have gotten same amount of responses, however in reality, during the parsing process many things can go wrong, for example:
- Bad internet connection might raise a requests error, passing straight to
except
- The website access can be limited, meaning that you can only open it 10 times in a minute, and considering that we are trying to parse it, we open it more than 1000 times oer minute.
- If the domain you are accessing is changing, it can simply not exist, raising an error once again.
So all of these are possible problems that might have occurred during the execution and because you used bare `except', you won't be able to identify what specifically went wrong and how to improve the quality of parsing.
You can catch multiple exceptions by specifying multiple except
blocks. Each except
block can handle a different type of exception in a different way.
try:
# Some code that might raise different types of exceptions
except ZeroDivisionError:
# Handle division by zero
except ValueError:
# Handle value errors
except Exception as e:
# Handle any other exceptions
print(f"An error occurred: {e}")
You can also catch multiple exception types in a single except
block by providing a tuple of exception types.
try:
# Some code that might raise different types of exceptions
except (ZeroDivisionError, ValueError) as e:
# Handle both ZeroDivisionError and ValueError
print(f"An error occurred: {e}")
In Python, the else
block can be used in conjunction with the try
and except
blocks to define a block of code that should only be executed if no exceptions were raised in the try
block. The else
block is an optional part of the error-handling mechanism and provides a clear way to separate the normal execution path from the error-handling code.
try:
result = 10 / 2
except ZeroDivisionError:
print("Cannot divide by zero!")
else:
print(f"The result is {result}")
The result is 5
In this example, the code inside the try
block successfully completes without raising any exceptions, so the else
block is executed, and the result is printed.
The else
block is useful when you want to separate the code that might raise an exception from the code that should
only be executed if no exceptions occur. This separation can improve the readability of your code and make it easier to understand the flow of execution.
Though, I must admit that the following approach is not really popular among Python society. Have no idea why, to be honest...
- The
else
block must follow allexcept
blocks and will only be executed if thetry
block does not raise an exception. - If an exception is raised in the
else
block, it will not be caught by the precedingexcept
blocks.
In Python, the finally
block is an important part of exception handling that is used in combination with try
and
except
blocks. The finally
block contains code that is guaranteed to execute, regardless of whether an exception is
raised in the try
block or not.
try:
print(10 / int(input("input your number:")))
except ZeroDivisionError:
print("0 is not accepted")
finally:
print("Thank you for using this program.")
In this example, 10 is attempted to be divided by user input in the try
block. If user inputs 0, a ZeroDivisionError
is raised and caught in the except
block. Regardless of whether the mathematical operation was successfully or not, the finally
block prints the closing message.
While Python and its libraries raise exceptions automatically under certain conditions, you also have the ability to manually raise exceptions in your code.
This is particularly useful for enforcing constraints, validating input, or signaling that a specific error condition has occurred.
The raise
statement allows you to throw an exception at any point in your program. When an exception is raised, it
interrupts the normal flow of the program and transfers control to the nearest enclosing try
block.
The basic syntax to raise an exception is:
raise ExceptionType("Description of the error.")
Where ExceptionType
is the class of the exception you want to raise (e.g., ValueError
, TypeError
, KeyError
), and the string provides a description of the error.
1.Input Validation: If a function requires inputs to meet certain conditions, you can raise an exception if the provided inputs are invalid.
def set_age(age):
if age < 0:
raise ValueError("Age cannot be negative")
try:
set_age(-5)
except ValueError as e:
print(e)
2.Enforcing Constraints: When certain states or conditions must not occur within a program, raising exceptions can enforce these constraints explicitly.
inventory = {'apple': 10, 'banana': 5}
def add_to_cart(item, quantity, cart):
if item not in inventory:
raise KeyError("Item not available")
if inventory[item] < quantity:
raise ValueError("Insufficient stock")
cart[item] = quantity
inventory[item] -= quantity
3.Signaling Unimplemented Features: If a part of your code is not yet implemented, you can raise a NotImplementedError
as a placeholder.
def future_feature():
raise NotImplementedError("This feature is coming soon!")
try:
future_feature()
except NotImplementedError as e:
print(e)
There are no limits for your imagination in terms of handling errors, again this will come with practice, just think about potential errors which might occur inside the application and try to handle them gracefully!
-
Be Specific: Prefer raising and catching specific exceptions rather than the general
Exception
class. This makes error handling more predictable, robust and optimised. -
Provide Useful Messages: When raising exceptions, include a clear, descriptive message to make it easier to understand the cause of the error.
-
Use Exceptions Judiciously: While exceptions are powerful, using them inappropriately can make your code harder to understand and maintain. Avoid using exceptions for normal flow control, and prefer using them for actual error conditions.
Exception chaining in Python is a mechanism that allows you to link exceptions together, making it easier to understand a sequence of errors that led to a failure.
This feature is particularly useful when an exception is raised while handling another exception.
Python automatically chains exceptions if an exception is raised inside an except
block. The original exception is
available in the __context__
attribute of the new exception.
try:
# This block intentionally raises a ZeroDivisionError
result = 1 / 0
except ZeroDivisionError:
try:
# This block will raise a NameError
print(unknown_variable)
except NameError as e:
raise RuntimeError("A NameError occurred") from e
In this example, the NameError
is implicitly chained to the RuntimeError
. Python will display both exceptions,
indicating that the RuntimeError
was directly raised while handling the NameError
.
You can explicitly chain exceptions using the from
keyword. This allows you to specify the cause of the exception,
which can be either another exception instance or None
to indicate that the chaining should be suppressed.
try:
# Some operation that fails
open("nonexistent_file.txt")
except FileNotFoundError as e:
# Explicitly chaining the exception
raise ValueError("Failed to open configuration.") from e
In this case, if the file does not exist, a FileNotFoundError
is raised, and it is explicitly chained to a
ValueError
. When the exception is caught, Python will indicate that the ValueError
was directly raised from the
FileNotFoundError
.
You can suppress exception chaining by specifying from None
. This is useful when the exception context is not helpful
or you want to prevent the display of chained exceptions.
try:
some_operation()
except SomeError as e:
raise DifferentError("An error occurred") from None
This prevents Python from chaining the exceptions, so only the DifferentError
is shown to the user, making the error
message cleaner.
What will the following code output?
try:
print(1 / 0)
except ZeroDivisionError:
print("You cannot divide by zero!")
A) You cannot divide by zero!
B) 1
C) An unhandled exception is thrown
D) 0
Which
except
clause will catch aTypeError
?
try:
'2' + 2
except ValueError:
print("ValueError caught!")
except TypeError:
print("TypeError caught!")
except:
print("Some other error caught!")
A) ValueError
B) TypeError
C) The generic except
block
D) No except
block catches the error
What is the output of the following code snippet?
try:
num = int("3")
except ValueError:
print("Not a number!")
else:
print("It is a number!")
A) Not a number!
B) It is a number!
C) Nothing is printed
D) An error is thrown
What does the
finally
block do?
try:
x = 1 / 0
except ZeroDivisionError:
print("Error!")
finally:
print("Will this be executed? Or error anyway?")
A) It will not execute if an exception is caught.
B) It is executed only if no exceptions occur.
C) It executes regardless of whether an exception was caught or not.
D) It will execute before the except
block.
What is the purpose of specifying multiple exceptions in a single
except
clause?
try:
# Code that might raise different errors
except (ZeroDivisionError, KeyError) as e:
print(f"Caught an error: {e}")
A) To handle different types of exceptions that might be raised in the same block of code.
B) To increase the processing time by handling all errors at once.
C) To handle only the first error that occurs and ignore others.
D) It's a syntax error to specify multiple exceptions.
How does exception chaining help in Python?
A) It suppresses all exceptions.
B) It links exceptions together, helping to trace back to the initial error.
C) It prevents exceptions from being raised.
D) It automatically resolves exceptions without user intervention.
Objective: Create a function safe_divide
that safely performs division and handles any division errors gracefully.
- The function should accept two parameters,
numerator
anddenominator
. - Use
try
andexcept
blocks to handle division errors such asZeroDivisionError
. - If a division by zero occurs, print an error message and return
None
. - If the division is successful, return the result.
- Use the
finally
block to print a message that the division attempt has been completed.
print(safe_divide(10, 2)) # Output: 5.0
print(safe_divide(5, 0)) # Output: Error: Cannot divide by zero
Objective: Implement a function check_voter_age
that checks if a person is eligible to vote and raises an exception if the age is below the minimum voting age.
- The function should accept one integer argument,
age
. - If
age
is less than 18, raise aValueError
with a message indicating that the person is too young to vote. - If
age
is 18 or above, print a message confirming that the person is allowed to vote. - Use a
try
block to test the function with different ages and anexcept
block to catch and print the ValueError message.
check_voter_age(21) # Output: You are allowed to vote.
check_voter_age(16) # Output: Error: You are too young to vote.