Error Handling and Exceptions
Errors happen: missing files, bad user input, network timeouts, division by zero. Business analysts need scripts that handle errors gracefully—log the issue, skip bad rows, retry failed requests—instead of crashing. Master error handling and your tools become production-ready.
Estimated reading time: 25–30 minutes
Core Error Handling
- try / except → catch errors, prevent crashes
- except Type → handle specific errors differently
- finally → cleanup code that always runs
- else → code that runs only if no error
Great for: robust scripts, production tools
Common Exception Types
- FileNotFoundError → missing file
- ValueError → invalid conversion (int("abc"))
- KeyError → missing dict key
- ZeroDivisionError → divide by zero
Great for: anticipating real-world failures
Try / Except — Catching Errors
Wrap risky code in try blocks. If an error occurs, except blocks handle it instead of crashing.
# Without error handling (crashes)
# result = 10 / 0 # ZeroDivisionError
# With error handling (graceful)
try:
result = 10 / 0
except ZeroDivisionError:
print("Error: Cannot divide by zero")
result = None
print("Script continues:", result)Handling Multiple Exception Types
Different errors need different responses. Use multiple except blocks or tuple syntax.
def safe_divide(a, b):
try:
return a / b
except ZeroDivisionError:
print("Error: Division by zero")
return None
except TypeError:
print("Error: Invalid types (need numbers)")
return None
print(safe_divide(10, 2)) # 5.0
print(safe_divide(10, 0)) # Error: Division by zero, None
print(safe_divide(10, "x")) # Error: Invalid types, None
# Tuple syntax for same handling
try:
value = int(input("Enter number: "))
except (ValueError, TypeError):
print("Invalid input")
value = 0Accessing Error Details
Use as e to capture the error object for logging or debugging.
try:
data = {"name": "Ana"}
print(data["age"]) # KeyError
except KeyError as e:
print("Missing key:", e)
print("Available keys:", list(data.keys()))Finally — Cleanup Code
finally blocks always run, even if an error occurs. Perfect for closing files or connections.
def read_file(filename):
f = None
try:
f = open(filename, "r")
return f.read()
except FileNotFoundError:
print("Error:", filename, "not found")
return None
finally:
if f:
f.close()
print("File closed")
content = read_file("data.txt")Else — Success-Only Code
else blocks run only if no exception occurred. Useful for separating success logic from error handling.
try:
value = int(input("Enter number: "))
except ValueError:
print("Invalid input")
else:
print("You entered:", value)
# Process valid input here
finally:
print("Input validation complete")Raising Exceptions
Use raise to trigger errors when business rules are violated.
def process_order(qty, price):
if qty < 0:
raise ValueError("Quantity cannot be negative")
if price < 0:
raise ValueError("Price cannot be negative")
return qty * price
try:
total = process_order(-5, 10)
except ValueError as e:
print("Order validation failed:", e)Custom Exceptions
Create domain-specific exceptions for clearer error handling.
class InvalidSKUError(Exception):
pass
def validate_sku(sku):
if not sku.startswith("SKU"):
raise InvalidSKUError("Invalid SKU format: " + sku)
return True
try:
validate_sku("ABC123")
except InvalidSKUError as e:
print("Validation error:", e)Cornerstone Project — Robust CSV Processor (step-by-step)
Build a CSV processor that handles missing files, bad data, invalid rows, and logs all errors. This is how production ETL scripts work—they do not crash on bad data, they skip it and report what went wrong.
Step 1 — Create sample CSV with bad data
Simulate real-world messiness: missing values, invalid types, extra columns.
import csv
# Create test data with issues
test_data = [
{"sku": "SKU001", "qty": "10", "price": "12.99"},
{"sku": "SKU002", "qty": "abc", "price": "24.50"}, # bad qty
{"sku": "INVALID", "qty": "5", "price": "8.75"}, # bad SKU
{"sku": "SKU003", "qty": "20", "price": ""}, # missing price
]
with open("orders.csv", "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=["sku", "qty", "price"])
writer.writeheader()
writer.writerows(test_data)Step 2 — Define validation functions
Each function raises specific errors for different validation failures.
class ValidationError(Exception):
pass
def validate_sku(sku):
if not sku.startswith("SKU"):
raise ValidationError("Invalid SKU format: " + sku)
return sku
def validate_qty(qty_str):
try:
qty = int(qty_str)
if qty <= 0:
raise ValidationError("Quantity must be positive: " + str(qty))
return qty
except ValueError:
raise ValidationError("Invalid quantity: " + qty_str)
def validate_price(price_str):
try:
price = float(price_str)
if price < 0:
raise ValidationError("Price cannot be negative: " + str(price))
return price
except ValueError:
raise ValidationError("Invalid price: " + price_str)Step 3 — Process CSV with error tracking
Track valid rows, errors, and line numbers for reporting.
def process_orders(filename):
valid_orders = []
errors = []
try:
with open(filename, "r") as f:
reader = csv.DictReader(f)
for line_num, row in enumerate(reader, start=2): # start=2 (header is line 1)
try:
# Validate each field
sku = validate_sku(row["sku"])
qty = validate_qty(row["qty"])
price = validate_price(row["price"])
# If all valid, add to results
valid_orders.append({
"sku": sku,
"qty": qty,
"price": price,
"total": qty * price
})
except ValidationError as e:
errors.append({
"line": line_num,
"row": row,
"error": str(e)
})
except KeyError as e:
errors.append({
"line": line_num,
"row": row,
"error": "Missing column: " + str(e)
})
except FileNotFoundError:
print("Error: File", filename, "not found")
return None, None
except PermissionError:
print("Error: No permission to read", filename)
return None, None
return valid_orders, errorsStep 4 — Generate error report
Show what succeeded and what failed with details.
def print_report(valid, errors):
print("Processing Report")
print("=" * 50)
print("Valid orders:", len(valid))
print("Errors:", len(errors))
if valid:
total_value = sum(o["total"] for o in valid)
print("Total value: $", round(total_value, 2))
if errors:
print("\nError Details:")
for err in errors:
print(" Line", err['line'], ":", err['error'])
print(" Data:", err['row'])
# Run it
valid, errors = process_orders("orders.csv")
if valid is not None:
print_report(valid, errors)Step 5 — Export valid orders and error log
Save clean data for processing, errors for review.
def export_results(valid, errors):
# Export valid orders
if valid:
with open("valid_orders.csv", "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=["sku", "qty", "price", "total"])
writer.writeheader()
writer.writerows(valid)
print("Valid orders saved to valid_orders.csv")
# Export error log
if errors:
with open("error_log.txt", "w") as f:
f.write("Order Processing Error Log\n")
f.write("=" * 50 + "\n\n")
for err in errors:
f.write("Line " + str(err['line']) + ": " + err['error'] + "\n")
f.write(" Data: " + str(err['row']) + "\n\n")
print("Errors saved to error_log.txt")
export_results(valid, errors)How this helps at work
- Production-ready → handles bad data without crashing
- Audit trail → error log shows exactly what failed and why
- Partial success → processes valid rows even if some fail
- Reusable pattern → adapt for any CSV import workflow
Key Takeaways
- try / except → catch errors, prevent crashes
- Multiple except blocks → handle different errors differently
- finally → cleanup code that always runs (close files, connections)
- raise → trigger errors when business rules are violated
- Custom exceptions → create domain-specific error types
- Cornerstone → robust CSV processor handles real-world messiness
Next Steps
You have mastered error handling. Next, explore testing and debugging to catch errors before they happen, or dive into logging to track errors in production systems.