Python for Profit

Building a Complete Inventory System

09

Bulletproof Your Code with Error Handling

The Big Idea

This chapter introduces professional error handling, making our application robust and user-friendly by using try...except blocks to anticipate and manage invalid user input gracefully.

Roadmap

  • The Fragility of Trust: We'll demonstrate how trusting the user to enter perfect data is a recipe for disaster and show the ValueError crash in action.

  • Catching Errors with try...except: Learn the fundamental Python pattern for handling exceptions, allowing our program to recover from errors instead of stopping.

  • A Dedicated Input-Validation Function: We'll create a new, powerful helper function, get_valid_numeric_input, to handle the logic of asking for a number until the user provides a valid one.

  • Refactoring for Robustness: We will replace the simple input() calls in our add_product and update_product functions with our new, bulletproof helper function.

  • Completing Part 1: Celebrate the completion of our powerful, feature-complete command-line tool.

Full Chapter Content

Why Our "Perfect" Program is Still Fragile

Our application is feature-complete. It can create, read, update, and delete inventory. But it has a critical vulnerability: it trusts the user completely.

What happens if, when adding a product, the user types "ten" instead of "10" for the quantity? Or "free" instead of a number for the price?

Let's see. Run the program and try to add a product. When it asks for the quantity, type abc.

Enter the quantity for New Laptop: abc
Traceback (most recent call last):
  File "main.py", line ..., in <module>
    add_product(inventory, CURRENCY)
  File "main.py", line ..., in add_product
    new_product = {"name": product_name, "quantity": int(quantity_str), "price": float(price_str)}
ValueError: invalid literal for int() with base 10: 'abc'

The program crashes. This red text is a traceback, Python's way of showing what went wrong. It tells us we got a ValueError because the int() function doesn't know how to convert the string 'abc' into a number. A professional application should never crash like this. It should inform the user of their mistake and ask them to try again.

Catching Errors with try...except

To handle this, we use a try...except block. It works exactly like it sounds:

  1. Python tries to run the code inside the try block.

  2. If an error (an exception) occurs, it immediately stops executing the try block and jumps to the except block.

  3. If no error occurs, the except block is skipped entirely.

Here's the pattern:

try:
    # Code that might cause an error
    user_input = input("Enter a number: ")
    value = int(user_input)
    print(f"You entered the number {value}")
except ValueError:
    # Code that runs ONLY if a ValueError happens
    print("That wasn't a valid number! Please try again.")

Step 1: Create a Reusable Input Validation Function

We need to get valid numeric input in both the add_product and update_product functions. Instead of putting a try...except block in both places, we'll follow the DRY principle and create a new helper function. This function's only job will be to ask the user for input in a loop until they enter a valid number.

Add this new helper function to the "Function Definitions" section of main.py:

def get_valid_numeric_input(prompt, input_type):
    """
    A robust helper function to get a valid integer or float from the user.
    'prompt' is the message shown to the user.
    'input_type' should be the function int or float.
    """
    while True: # Loop forever until we get valid input
        user_input = input(prompt)
        try:
            # Try to convert the user's input to the desired type
            numeric_value = input_type(user_input)
            return numeric_value # Exit the loop and return the valid number
        except ValueError:
            # This block runs if the conversion fails
            print("Invalid input. Please enter a valid number.")

What's New Here?

  • A while True loop: This creates an infinite loop that will only be broken when the return statement is executed.

  • A function as a parameter: We are passing the actual Python functions int or float as the input_type parameter. This allows us to use the same helper for both kinds of numbers.

  • Returning from the loop: Once the input_type(user_input) line succeeds without a ValueError, the return statement is executed, sending the valid number back and ending the loop.

Step 2: Refactor add_product to be Bulletproof

Now we can replace the fragile input() calls in add_product with calls to our new, robust helper function.

Find add_product in main.py and modify it:

# Find this function in your code
def add_product(inventory_list, currency_symbol):
    """Interactively asks for a new product and adds it as a dictionary."""
    print("\n--- Add a New Product ---")
    product_name = input("Enter the new product name: ")

    # --- THESE LINES ARE CHANGING ---
    # Old, fragile way:
    # quantity_str = input(f"Enter the quantity for {product_name}: ")
    # price_str = input(f"Enter the price per item (in {currency_symbol}): ")
    # quantity = int(quantity_str)
    # price = float(price_str)

    # New, robust way:
    quantity = get_valid_numeric_input(f"Enter the quantity for {product_name}: ", int)
    price = get_valid_numeric_input(f"Enter the price per item (in {currency_symbol}): ", float)

    new_product = {
        "name": product_name,
        "quantity": quantity,
        "price": price
    }

    inventory_list.append(new_product)
    print(f"\nSUCCESS: '{product_name}' has been added to the inventory.")

Step 3: Refactor update_product

We need to do the same thing for our update_product function to protect it from bad input.

Find update_product and modify the input-gathering part:

# Find this function in your code
def update_product(inventory_list, currency_symbol):
    """Finds a product and allows the user to update its details."""
    index, product = find_product(inventory_list)
    if product is None:
        print("Product not found.")
        return

    print(f"Found: {product['name']}. Current quantity: {product['quantity']}, Current price: {currency_symbol}{product['price']}")

    new_quantity_str = input(f"Enter new quantity (or press Enter to keep {product['quantity']}): ")
    new_price_str = input(f"Enter new price (or press Enter to keep {product['price']}): ")

    # --- ADD ERROR HANDLING HERE ---
    if new_quantity_str:
        try:
            product['quantity'] = int(new_quantity_str)
        except ValueError:
            print("Invalid quantity. Keeping original value.")

    if new_price_str:
        try:
            product['price'] = float(new_price_str)
        except ValueError:
            print("Invalid price. Keeping original value.")

    print(f"SUCCESS: '{product['name']}' has been updated.")

Here, we use a slightly different approach. Since the user can press Enter to skip, we only enter the try...except block if they provided input. This makes the update function much more user-friendly.

The Final Code for Part 1

Congratulations! You have now completed the entire command-line tool. It is feature-complete, robust, and well-structured. This is a professional piece of software.

Here is the complete, final main.py script for Part 1:

# --- PyInventory: A Step-by-Step Journey to Profit ---
# Chapter 9: Bulletproof Your Code with Error Handling
# --- PART 1: COMPLETE CLI TOOL ---

import json

# --- Configuration ---
CURRENCY = "MAD"
DATA_FILE = "inventory.json"

# --- Function Definitions ---

def save_inventory_to_file(inventory_list, filename):
    with open(filename, 'w') as file:
        json.dump(inventory_list, file, indent=4)

def load_inventory_from_file(filename):
    try:
        with open(filename, 'r') as file:
            return json.load(file)
    except FileNotFoundError:
        return []

def get_valid_numeric_input(prompt, input_type):
    """A robust helper function to get a valid integer or float from the user."""
    while True:
        user_input = input(prompt)
        try:
            numeric_value = input_type(user_input)
            return numeric_value
        except ValueError:
            print("Invalid input. Please enter a valid number.")

def find_product(inventory_list):
    """Helper function to find a product by name and return its index and data."""
    search_name = input("Enter the name of the product to find: ")
    for index, product in enumerate(inventory_list):
        if product['name'].lower() == search_name.lower():
            return index, product
    return None, None

def display_inventory(inventory_list, currency_symbol):
    print("\n--- Current Inventory Report ---")
    print("-" * 60)
    total_inventory_value = 0.0
    if not inventory_list:
        print("Inventory is currently empty.")
    else:
        print(f"{'Product':<30} | {'Quantity':<10} | {'Value':>15}")
        print("-" * 60)
        for item in inventory_list:
            item_value = item['quantity'] * item['price']
            total_inventory_value += item_value
            print(f"{item['name']:<30} | {item['quantity']:<10} | {currency_symbol}{item_value:>14.2f}")
    print("-" * 60)
    print(f"GRAND TOTAL INVENTORY VALUE: {currency_symbol}{total_inventory_value:>40.2f}")
    print("-" * 60)

def add_product(inventory_list, currency_symbol):
    """Interactively asks for a new product and adds it as a dictionary."""
    print("\n--- Add a New Product ---")
    product_name = input("Enter the new product name: ")
    quantity = get_valid_numeric_input(f"Enter the quantity for {product_name}: ", int)
    price = get_valid_numeric_input(f"Enter the price per item (in {currency_symbol}): ", float)
    new_product = {"name": product_name, "quantity": quantity, "price": price}
    inventory_list.append(new_product)
    print(f"\nSUCCESS: '{product_name}' has been added to the inventory.")

def update_product(inventory_list, currency_symbol):
    """Finds a product and allows the user to update its details."""
    index, product = find_product(inventory_list)
    if product is None:
        print("Product not found.")
        return
    print(f"Found: {product['name']}. Current quantity: {product['quantity']}, Current price: {currency_symbol}{product['price']}")
    new_quantity_str = input(f"Enter new quantity (or press Enter to keep {product['quantity']}): ")
    new_price_str = input(f"Enter new price (or press Enter to keep {product['price']}): ")
    if new_quantity_str:
        try:
            product['quantity'] = int(new_quantity_str)
        except ValueError:
            print("Invalid quantity. Keeping original value.")
    if new_price_str:
        try:
            product['price'] = float(new_price_str)
        except ValueError:
            print("Invalid price. Keeping original value.")
    print(f"SUCCESS: '{product['name']}' has been updated.")

def delete_product(inventory_list):
    """Finds and deletes a product from the inventory."""
    index, product = find_product(inventory_list)
    if product is None:
        print("Product not found.")
        return
    product_name = product['name']
    inventory_list.pop(index)
    print(f"SUCCESS: '{product_name}' has been deleted from the inventory.")

# --- Main Application Logic ---
inventory = load_inventory_from_file(DATA_FILE)
while True:
    print("\n--- PyInventory Main Menu ---")
    print("1: Display Current Inventory")
    print("2: Add a New Product")
    print("3: Update a Product")
    print("4: Delete a Product")
    print("q: Quit")
    choice = input("Please enter your choice (1, 2, 3, 4, or q): ")
    if choice == '1':
        display_inventory(inventory, CURRENCY)
    elif choice == '2':
        add_product(inventory, CURRENCY)
    elif choice == '3':
        update_product(inventory, CURRENCY)
    elif choice == '4':
        delete_product(inventory)
    elif choice.lower() == 'q':
        save_inventory_to_file(inventory, DATA_FILE)
        print("Inventory saved. Exiting PyInventory. Goodbye!")
        break
    else:
        print("Invalid choice. Please try again.")


Chapter 9: Summary & End of Part 1

You've built something incredible. You have progressed from a simple print() statement to a fully functional and robust command-line application.

  • You learned how to prevent crashes using try...except blocks.

  • You created a powerful, reusable input validation function.

  • You refactored your code to make it bulletproof against user error.

You have mastered the fundamentals of Python. You are ready for the next stage of your journey.

In Part 2: The Sellable Desktop App with a Real Database, we will leave the command line behind. We will learn why file-based storage has its limits and upgrade our system to use a real, professional database. We'll then build a beautiful graphical user interface (GUI), turning our tool into a polished desktop application that you could sell to a client.