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
ValueErrorcrash 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 ouradd_productandupdate_productfunctions 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:
-
Python tries to run the code inside the
tryblock. -
If an error (an exception) occurs, it immediately stops executing the
tryblock and jumps to theexceptblock. -
If no error occurs, the
exceptblock 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 Trueloop: This creates an infinite loop that will only be broken when thereturnstatement is executed. -
A function as a parameter: We are passing the actual Python functions
intorfloatas theinput_typeparameter. 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 aValueError, thereturnstatement 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...exceptblocks. -
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.