Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 5

Exception Handling in Python

Recommended resources

Hello! Let's dive into the next lesson in your Python Programming Foundations module.

In our last session, we explored inheritance and polymorphism, discovering how to create specialized child classes from a parent class. This ability to create class hierarchies is not just for organizing code—it's a powerful mechanism that Python uses for many of its core features.

Today, we'll see a direct application of that. We'll focus on how to make your programs more robust and predictable by managing errors. The learning outcome for this lesson is to handle exceptions using try-except blocks and create custom exceptions. You'll learn how to anticipate and manage potential failures gracefully, preventing your applications from crashing unexpectedly.

1. Why We Need Error Handling

Imagine your program tries to open a file that doesn't exist or divide a number by zero. Without any error handling, the program would simply crash, showing a user-unfriendly "traceback" message. This is where exception handling comes in. It allows you to "try" a block of code that might fail and, if it does, "catch" the error and execute a fallback plan.

Let's watch a video that introduces this fundamental concept.

Python Tutorial: Using Try/Except Blocks for Error Handling

This video from Corey Schafer provides an excellent, practical introduction to why we need try-except blocks and how they work.

Watch the first two minutes (00:00 - 02:07). Notice how an unhandled FileNotFoundError crashes the program, and how wrapping the code in a try block allows you to catch the error and display a friendly message instead.


2. The try...except Block and Its Variations

The basic structure you just saw is the try-except block. However, there's more to it. A robust error handling structure can include else and finally blocks, giving you fine-grained control over your program's flow.

  • try: The block of code to be attempted.
  • except: The block of code that runs only if an exception occurs in the try block.
  • else: The block of code that runs only if no exception occurs in the try block.
  • finally: The block of code that always runs, regardless of whether an exception occurred. This is perfect for cleanup actions, like closing a file or a database connection.
Python Try-Except-Else-Finally Flow
This diagram from Real Python illustrates the complete control flow of an exception handling block. The path your code takes depends on whether an exception is raised in the `try` block.

Let's continue with the video to see these components in action and learn a crucial best practice: being specific about the exceptions you catch.

Python Tutorial: Using Try/Except Blocks for Error Handling

This next segment of the video covers how to handle multiple, specific exceptions and explains the roles of the else and finally blocks.

Watch from 02:07 to 08:35. Pay close attention to: Catching Specific Exceptions: Why except FileNotFoundError: is better than a generic except Exception:. Catching specific errors prevents you from accidentally hiding bugs you didn't anticipate. Handling Multiple Exceptions: How you can stack except blocks to handle different types of errors. The else Block: How it's used for code that should only run if the try block was successful. The finally Block: Why it's essential for cleanup code that must execute no matter what.


3. Creating Your Own Exceptions

While Python has many built-in exceptions (ValueError, TypeError, FileNotFoundError, etc.), sometimes you need to represent an error that is specific to your application's logic. For example, in a banking application, you might want an InsufficientFundsError.

This is where you'll apply what you learned about inheritance. A custom exception is simply a Python class that inherits from the built-in Exception class.

Creating and Raising Custom Exceptions in Python Flowchart
This flowchart shows the four main steps to creating and using a custom exception: define a class that inherits from `Exception`, optionally add an `__init__` method for custom messages, and use the `raise` keyword to trigger it.

Let's watch a video that explains the motivation and process for creating custom exceptions.

Why You Need Custom Exception Classes

This video from ArjanCodes clearly explains why you'd want to create a custom exception and walks through how to do it by subclassing Exception.

Watch from the beginning to 04:29. Focus on: The limitations of using generic exceptions like ValueError. How a custom exception is defined: class WeightLimitExceededError(Exception):. How you can add an __init__ method to your custom exception to store and pass along specific data about the error.


4. Raising Exceptions

You've seen how to catch exceptions that Python raises automatically. You can also trigger exceptions manually using the raise keyword. This is useful when you detect a condition in your code that makes it impossible to proceed, like a user providing invalid input to a function.

You can raise built-in exceptions or the custom ones you've created.

Python Try Except: Examples And Best Practices

Let's read a short article that provides a great example of defining, raising, and catching a custom exception. This will solidify the concepts from the video.

Read the sections titled 'Create custom exceptions' and 'Raising (or throwing) exceptions'. The UserNotFoundError example is a perfect illustration of why creating a custom exception makes your code more readable and robust than simply using raise Exception('User not found').


5. Practice: A Simple Inventory System

Time to combine these concepts. You will build a simple Inventory class that uses both built-in and custom exceptions to handle errors.

Instructions:

  1. Create a custom exception named ItemNotFoundError that inherits from Exception. A simple pass in the class body is sufficient.
  2. Create an Inventory class with the following:
    • An __init__ method that initializes a dictionary to store inventory items and their quantities, e.g., self.stock = {"laptop": 10, "mouse": 50}.
    • A method remove_item(item_name, quantity).
  3. Implement the logic for remove_item:
    • If item_name is not in self.stock, raise your custom ItemNotFoundError with a helpful message.
    • If the quantity to remove is greater than the available stock for that item, raise a built-in ValueError with a helpful message.
    • If all checks pass, decrease the stock quantity for the item.
  4. Write test code outside the class that calls remove_item and uses try-except blocks to gracefully handle both ItemNotFoundError and ValueError.



# 1. Define your custom exception here
# class ItemNotFoundError...




# 2. Define the Inventory class here
class Inventory:
    def __init__(self):
        self.stock = {"laptop": 10, "mouse": 50, "keyboard": 25}
        print(f"Initial stock: {self.stock}")

    def remove_item(self, item_name, quantity):



        # 3. Implement the logic here
        pass




# --- 4. Test your implementation ---
inventory = Inventory()




# Test case 1: Successful removal
try:
    inventory.remove_item("mouse", 5)
    print(f"Successfully removed 5 mice. Current stock: {inventory.stock}")
except (ValueError, NameError): # Replace NameError with your custom exception
    print("This should not happen for this test case.")




# Test case 2: Item not found
try:
    inventory.remove_item("webcam", 1)
except NameError as e: # Replace NameError with your custom exception
    print(f"Error: {e}")




# Test case 3: Insufficient stock
try:
    inventory.remove_item("laptop", 20)
except ValueError as e:
    print(f"Error: {e}")




# Final stock check
print(f"Final stock: {inventory.stock}")

Click here to see a possible solution



# 1. Define your custom exception
class ItemNotFoundError(Exception):
    """Raised when an item is not found in the inventory."""
    pass




# 2. Define the Inventory class
class Inventory:
    def __init__(self):
        self.stock = {"laptop": 10, "mouse": 50, "keyboard": 25}
        print(f"Initial stock: {self.stock}")

    def remove_item(self, item_name, quantity):



        # 3. Implement the logic
        if item_name not in self.stock:
            raise ItemNotFoundError(f"Item '{item_name}' not found in inventory.")
        
        if quantity > self.stock[item_name]:
            raise ValueError(f"Cannot remove {quantity} of '{item_name}'. Only {self.stock[item_name]} available.")
            
        self.stock[item_name] -= quantity




# --- 4. Test your implementation ---
inventory = Inventory()




# Test case 1: Successful removal
try:
    inventory.remove_item("mouse", 5)
    print(f"Successfully removed 5 mice. Current stock: {inventory.stock}")
except (ValueError, ItemNotFoundError):
    print("This should not happen for this test case.")



# Expected output: Successfully removed 5 mice. Current stock: {'laptop': 10, 'mouse': 45, 'keyboard': 25}

print("-" * 20)




# Test case 2: Item not found
try:
    inventory.remove_item("webcam", 1)
except ItemNotFoundError as e:
    print(f"Error: {e}")



# Expected output: Error: Item 'webcam' not found in inventory.

print("-" * 20)




# Test case 3: Insufficient stock
try:
    inventory.remove_item("laptop", 20)
except ValueError as e:
    print(f"Error: {e}")



# Expected output: Error: Cannot remove 20 of 'laptop'. Only 10 available.

print("-" * 20)



# Final stock check
print(f"Final stock: {inventory.stock}")



# Expected output: Final stock: {'laptop': 10, 'mouse': 45, 'keyboard': 25}

Conclusion

Excellent work! You've learned how to build more resilient Python applications by anticipating and handling errors. This is a fundamental skill for any developer, as real-world data and user interactions are often unpredictable.

Here are the key takeaways from today's lesson:

  • The try-except-else-finally structure provides a robust way to control program flow when errors occur.
  • Always be specific about the exceptions you catch to avoid masking unexpected bugs.
  • Custom exceptions are created by inheriting from Python's Exception class and allow you to create meaningful, application-specific error types.
  • The raise keyword lets you manually trigger an exception when your code detects a problem.

In our next lesson, we'll begin working with files. You'll learn how to read, write, and process data from common formats like CSV and JSON. The exception handling skills you've just acquired will be immediately useful, as file operations are a common source of errors like FileNotFoundError and PermissionError.