• Labs icon Lab
  • Core Tech
Labs

Guided: Exception Handling and Modular Programming in Python

Embark on a journey through the realm of Python with this lab focused on exception handling and modular programming. This code lab will equip you with essential skills to ensure your Python programs run smoothly and is easily maintainable. In this code lab, you'll delve deep into the art of handling exceptions, learning how to gracefully manage errors and unexpected situations in your code. Gain expertise in crafting robust and reliable programs as you master try-except blocks and error-handling strategies. But that's not all – you'll also unravel the power of modular programming, showing you how to break down your code into reusable, organized modules. Explore the world of functions, modules, and packages, and understand how they can streamline your Python projects and make your code more manageable.

Labs

Path Info

Level
Clock icon Beginner
Duration
Clock icon 40m
Published
Clock icon Nov 01, 2023

Contact sales

By filling out this form and clicking submit, you acknowledge our privacy policy.

Table of Contents

  1. Challenge

    Introduction

    Welcome to the Guided: Exception Handling and Modular Programming in Python lab.

    In the world of programming, there's more to building robust and efficient applications than just data manipulation. You need to ensure that your code is foolproof and adaptable. This is where exception handling and modular programming come into play, providing extra layers of support to promote code reusability, maintainability, and functionality .


    Resilient Recipes

    Imagine your digital cookbook app is now a well-established kitchen companion. Users rely on it to create and store all of their culinary ideas. But what happens when unexpected events occur? What if a user mistakenly inputs an impossible recipe, an invalid value, or the system encounters an error while saving a recipe?

    Exception handling in Python is your safety net. It catches and gracefully handles unexpected mishaps, ensuring your application continues to run smoothly. It's your insurance policy for those unforeseen hiccups in your code.


    Modular Cooking

    As your cookbook grows, you'll find yourself juggling an extensive array of recipes and features. It becomes crucial to keep your code organized, maintainable, and easy to expand. This is where modular programming steps in, offering you a structured approach to breaking down your application into manageable, reusable components.


    Hands-On

    You're invited to experiment and learn interactively. Make use of the Terminal to the right to execute the cookbook application by clicking the Run button or by entering the command python cookbook.py. If you wish to create another file to test out some things, you can also run that in the Terminal with python filename.py.

  2. Challenge

    Exception Handling in Python

    Understanding Exceptions

    In Python, exceptions are unexpected events that can disrupt the normal flow of your program. These events can range from trying to access a file that doesn't exist to performing mathematical operations that are mathematically impossible. Exception handling is the technique of anticipating, identifying, and managing these exceptions to prevent your program from crashing, typically through the use of a try-except clause.

    Consider this example:

    try:
        result = 10 / 0
    except ZeroDivisionError:
        result = "Division by zero is not allowed"
    

    In this code snippet, an attempt is made to divide by 0, which would cause an exception and normally result in the program crashing. By using the try-except clause to catch the ZeroDivisionError, the exception is caught, allowing the program to continue without crashing.


    Try-Except

    The try-except is the key to adding exception handling to your code. As seen in the example above, you encase any code that could raise an exception within the try clause. If an exception is raised, the code in the try clause will immediately stop. If the exception is caught in the except clause, then the code within the except clause will be executed.

    You can also add an else or a finally clause to execute after the try-except block. The else clause will only execute after the try clause if no exception was caught. On the other hand, finally will always execute whether or not an exception was caught. This makes finally especially useful for some tasks such as cleanup tasks. Here are some examples:

    try:
        x = 10
        y = 0
        z = x / y
    except ZeroDivisionError:
        print("Cannot divide by zero!") # If y = 0, this exception would be caught and "Cannot divide by zero!" is printed. z will not be printed.
    else:
        print(z) # If y != 0, z is printed to console
    
    try:
        x = 10
        y = 0
        z = x / y
    except ZeroDivisionError:
        print("Cannot divide by zero!") 
    finally:
        print(x) # Always prints x regardless of whether y=0
    

    Raising Exceptions

    Aside from using try-except blocks, you can also utilize the raise statement to force exceptions to occur. Let's take a look at another example

    x = 15
    y = 0
    
    if y == 0:
        raise ValueError("y cannot be 0!")
    
    z = x / y
    

    As seen in the example, if y was 0 but the if statement did not exist, the line z = x / y would raise a ZeroDivisionError exception. However, since the if statement is there, if y was 0 it would actually result in a ValueError exception that is forcibly raised with the raise statement instead of the ZeroDivisionError.


    Multiple Exceptions and Best Practices

    You can also have multiple except clauses for a single try clause. However, only the first except clause to match the raised exception will execute. This behavior is the same as a switch statement. Furthermore, except clauses can catch multiple types of exceptions and alias them when caught. Take a look at the following example:

    try:
        # Code that could raise an exception here
    except (ExceptionType1, ExceptionType2) as Ex1: # Caught exception is aliased as Ex1
        print(Ex1)
    except (ExceptionType3, ExceptionType4) as Ex2: # Caught exception is aliased as Ex2
        print(Ex2)
    except Exception as e: # Generic exception is aliased as e
        print(e)
    

    So let's unpack this. Notice that there are 3 except clauses for the try clause and the first 2 except clauses have multiple exception types. This means that if an exception of type ExceptionType1 or ExceptionType2 is raised, only the first except clause executes. If the exception is of type ExceptionType3 or ExceptionType4, only the second except clause executes. Lastly, if the exception is none of the aforementioned types, then the 3rd except clause will execute.

    Notice that the generic Exception catch comes last. If it came first, the other two except clauses would never execute because the generic would match every exception thrown. This is why you should always put the most specific except clauses first before generic or higher order exception types. This is know as an exception hierarchy.

    One last thing to note is that in a production setting, you want to avoid using catch-alls such as except Exception, except BaseException, or even a blanket except without a specified exception type because these can potentially obfuscate particular details that you need to know about the exceptions being caught. This can result in serious bugs and security risks stemming from underlying issues in your code. Remember that exception handling is meant to address potential or expected errors in code, not the unexpected.

  3. Challenge

    Modular Programming in Python

    Modules in Python

    A module in Python is a file containing Python definitions and statements. You can think of a module as a library of functions and variables that can be imported and used in your programs. Here's how you can create and use a simple module:

    # mymodule.py
    
    def greet(name):
        return f"Hello, {name}!"
    
    # main.py
    
    import mymodule
    
    message = mymodule.greet("Alice")
    print(message)  # Output: "Hello, Alice!"
    

    Modules help you organize your code, making it more readable and allowing you to reuse functions across different parts of your project.


    Packages in Python

    Packages are a way to organize related modules into directories, creating a hierarchical structure for your project. A package is a directory that contains a special __init__.py file to indicate that it's a package. Here's a simple example:

    mypackage/
        __init__.py
        module1.py
        module2.py
    

    Now, you can import modules from the package:

    # main.py
    
    from mypackage import module1
    
    result = module1.add(3, 4)
    print(result)  # Output: 7
    

    Packages provide a powerful way to compartmentalize your codebase, especially in larger projects, by allowing you to group related modules and submodules together for better maintainability and readability.


    The init.py file

    The __init__.py file is a special file within a package directory. It can contain initialization code, package-level variables, and it's executed when the package is imported. While it can be empty, it often contains package-level setup code. Here's an example:

    # __init__.py
    
    print("mypackage has been imported")
    
    # main.py
    
    from mypackage import module1
    

    When you import mypackage, the code in __init__.py will execute, allowing you to perform package-level setup. The __init__.py file must exist within a directory for Python to recognize it as a package.

  4. Challenge

    Adding Exception Handling to the Cookbook

    Now that you are familiar with exception-handling and modularity in Python, let's take a look at the cookbook application in cookbook.py. This application is fully functional as it is now, but the load_recipes(), save_recipes(), and add_recipe() functions are susceptible to exceptions that could cause the application to crash. You will be adding some simple exception-handling to remedy this.

    If you're stuck or want to compare your results, check out the files in the solution directory. There is also a copy of the original recipes.json in case you need it to reset the contents of your recipes.json.

    There are multiple "correct" answers here, so don't be alarmed if your solution is slightly different. What matters is that the application should be working as intended!


    Handling load_recipes()

    Let's take a look at the load_recipes() function. This function uses the open() method to open a file with the specified filename parameter and load it into a Python object. The default filename it loads is recipes.json, which is a valid file. However, if a nonexistent filename was provided, this function would throw an exception and crash. Let's add exception-handling to fix that.

    Details 1. Use a `try-except` block here. 2. The exception to be caught is a `FileNotFoundError`. 3. In the `except` clause, return an empty List.

    Handling save_recipes()

    Next, let's look at save_recipes(). Similarly to load_recipes(), it utilizes the open() method to open a file and write to it. If an nonexistent filename is given, this method will generate a file with that filename and dump recipes into it. You don't want that, so let's make some adjustments.

    Details 1. Use a `try-except-else` block here. 2. Before the `with open()` statement, add an `if` statement that checks if `filename` exists using `os.path.exists(filename)`. 3. If the `filename` does not exist, use `raise` to throw a `FileNotFoundError`. Pass in a format string such as `f"File with the name {filename} does not exist."` or something similar to the exception. 4. In the `except` clause, catch the `FileNotFoundError` and print its contents. A format string such as `f"Error: {e}. Recipes was not updated."` works here. 5. In the `else` clause, print a success message like `"Recipes saved successfully!"`.

    Handling add_recipe()

    Lastly, fix the add_recipe() function. The prep_time and rating variables attempt to cast the user input to an integer, but what happens if the input was a word such as test? This would throw an exception because a word cannot be cast as an integer! To address this, use a try-except-else-finally block.

    Details 1. Encase everything up to and including `recipes.append(recipe)` within the `try` clause. 2. The `except` clause should catch a `ValueError` if input that cannot be parsed as an integer is given. You can `print` something simple such as `f"Error: {e}"`. 3. In the `else` clause, place the `print("Recipe added successfully!")` statement. 4. In the `finally` clause, return `recipes`. A format string such as `f"Error: {e}. Recipes was not updated."` works here.

    With this, you've added exception handling to the application. If a nonexistent filename is given to load_recipes(), the function will return an empty List instead of crashing the application. If given to save_recipes(), the function will no longer extra unwanted files with recipes saved into them. Finally, invalid inputs for prep_time and rating will instead reprompt the user from the menu instead of crashing the application as well.

  5. Challenge

    Modularizing the Cookbook

    You have one last thing to do and that is to refactor the code in cookbook.py. Notice how there are five functions related to recipes and then the main function. If you were to add more features or expand upon the functionality for recipes, the code would quickly become bloated and difficult to read. Instead, let's make it more modular by moving the recipe functions into a package.


    Creating a Package

    To create a package, first make a new directory in the workspace. Call it recipe_manager or any other name of your choosing, though you'll have to adjust some import statements in the following steps accordingly.

    As mentioned previously, a directory will only be recognized as a package if it has an __init__.py file, even if the file is empty. Go ahead and make one and you will have successfully created a package to move your code into.


    Refactoring into Modules

    Within your newly created package, create a module called recipe_operations.py or any other name that you wish. Move the method definitions for load_recipes(), save_recipes(), add_recipe(), view_recipes(), and search_by_ingredient() into your new module. Now, cookbook.py should look much cleaner as it should only contain the main() method.

    Don't forget that since you have refactored the recipe functions, you need to import them from your package and module. At the top of cookbook.py, add an import statement along the lines of from recipe_manager.recipe_operations import *, which will import everything in the recipe_operations module of the recipe_manager package. Make sure to use the correct names if you used different names for your package and module.


    Just like that, you're done! You've added exception handling to your cookbook application to make it more robust and refactored some of its code into modular components. This will set a solid foundation for any future additions or features you'd like to add. Feel free to tinker with the application as you wish, or run it for yourself!

George is a Pluralsight Author working on content for Hands-On Experiences. He is experienced in the Python, JavaScript, Java, and most recently Rust domains.

What's a lab?

Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.

Provided environment for hands-on practice

We will provide the credentials and environment necessary for you to practice right within your browser.

Guided walkthrough

Follow along with the author’s guided walkthrough and build something new in your provided environment!

Did you know?

On average, you retain 75% more of your learning if you get time for practice.