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 ouradd_product
andupdate_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:
Python tries to run the code inside the
try
block.If an error (an exception) occurs, it immediately stops executing the
try
block and jumps to theexcept
block.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 thereturn
statement is executed.A function as a parameter: We are passing the actual Python functions
int
orfloat
as theinput_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 aValueError
, thereturn
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.