Skip to content

Latest commit

 

History

History
481 lines (343 loc) · 15.5 KB

11.Exceptions.md

File metadata and controls

481 lines (343 loc) · 15.5 KB

Lesson 11: Exceptions

"Exceptions are the gentle reminders that perfection is a journey, not a destination."

Content

  1. Introduction
  2. The try & except Block
  3. The else Block
  4. The finally Block
  5. Raising Exceptions
  6. Exception Chaining
  7. Quiz
  8. Homework

1 Introduction

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.

2 The try & except Block

In python, error handling is managed with the use of try and except keywords.

2.1 How do try and except work?

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.

Example

try:
    result = 10 / 0  # This will raise a ZeroDivisionError
except ZeroDivisionError:
    print("Cannot divide by zero!")

Output

Cannot divide by zero!

Explanation

The code inside the try block raises a ZeroDivisionError. The except block catches this exception and prints a message, without crashing the programm.

2.2 except Exception as e

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.

Example

try:
    print(3 / 0) # this will raise ZeroDivisionError 
except Exception as e:
    print(f"An error occurred: {e}")

Output

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.

Example

responses = []

for i in range(0, 9999):
    try:
        response = requests.get("http://example.com")
        responses.append(response)
    except:
        pass

print(len(responses))

Output

6785

Explanation

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.

2.3 Catching Multiple Exceptions

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}")

2.4 Multiple Exception Types

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}")

3 The else Block

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.

Example

try:
    result = 10 / 2
except ZeroDivisionError:
    print("Cannot divide by zero!")
else:
    print(f"The result is {result}")

Output

The result is 5

Explanation

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.

3.1 When to use else

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...

3.2 Note

  • The else block must follow all except blocks and will only be executed if the try block does not raise an exception.
  • If an exception is raised in the else block, it will not be caught by the preceding except blocks.

4 The finally Block

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.

Example:

try:
    print(10 / int(input("input your number:")))
except ZeroDivisionError:
    print("0 is not accepted")
finally:
    print("Thank you for using this program.")

Explanation

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.

5 Raising Exceptions

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.

5.1 Understanding raise

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.

5.2 Syntax of raise

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.

5.3 Use Cases

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!

5.5 Best Practices

  • 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.

6 Exception Chaining

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.

6.1 Implicit Exception Chaining

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.

Example

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

Explanation

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.

6.2 Explicit Exception Chaining with from

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.

Example

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

Example

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.

6.3 Suppressing Exception Chaining

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.

Example of Suppressing Exception Chaining:

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.

7. Quiz

Question 1:

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


Question 2:

Which except clause will catch a TypeError?

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


Question 3:

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


Question 4:

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.


Question 5:

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.

Question 6:

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.

8. Homework

Task 1: Safe Division

Objective: Create a function safe_divide that safely performs division and handles any division errors gracefully.

Requirements:

  • The function should accept two parameters, numerator and denominator.
  • Use try and except blocks to handle division errors such as ZeroDivisionError.
  • 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

Task 2: Voting

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.

Requirements:

  • The function should accept one integer argument, age.
  • If age is less than 18, raise a ValueError 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 an except 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.