6  Reflect on What You’ve Learned

Reflect full slide

Now that you’ve fixed the bug, it’s not time to move on just yet. With a little more investment of time, you can consolidate your learning. This will help you to avoid or recognize similar bugs in the future.

Of course, if this bug was easy for you to fix, you might not need to spend much time on this. But if you found it challenging, it’s worth spending some time to reflect on what you’ve learned.

6.1 Strategy

6.1.1 Add to Your Bug Log and Update Your Bug Recipes

We encourage you to maintain two documents:

  • a “bug log” that summarizes specific debugging problems you’ve encountered and how you fixed them.
  • a “bug recipe book” that summarizes general strategies for debugging when encountering certain symptoms.

We have included a sample of each of these below. You can find more completed example entries in the book sections titled “Bug Log” and “Bug Recipe Book”.

6.1.1.1 Sample Bug Log Entry

Code
def double(x):
   x * 2
4 + double(3)
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

Cell In[1], line 3

      1 def double(x):

      2    x * 2

----> 3 4 + double(3)



TypeError: unsupported operand type(s) for +: 'int' and 'NoneType'
Field Description
Symptom TypeError: unsupported operand type(s) for +: ‘int’ and ‘NoneType’
Proximal Cause data fault: double(3) has the value None.
Root Cause double(x)returns valueNone`
Fixes Added the missing return statement to the function.
Code
def double(x):
   return x * 2
4 + double(3)
10

6.1.1.2 Sample Bug Recipe Entry

Bug Name NoneType
Symptoms Runtime error with message that mentions ‘NoneType’ object
Potential Proximal Causes
  • data fault: a data object that I expect to have some other value has the value None.
Potential Root Causes
  • The data object is the result of a function call that failed to return a value.
  • A function has an optional parameter with a default value of None and was invoked without a value for that parameter.
Potential Fixes
  • Added the missing return statement to the function.
  • Provide a value for the optional parameter.
  • Make the code work gracefully with None values.

6.1.2 Future Proof Your Code

In addition to maintaining a bug book, you can also take steps to future-proof your code. Based on what you observed during this debugging session, can you identify similar errors that might occur in the future? If so, you can add assert statements that will detect any such errors as soon as they occur, making it easier to debug them. You may also be able to add error handling code that will allow you to automatically handle such problems.

  • Identify side effects. What could go wrong as a result of this fix?
  • Identify technical debt. What else could go wrong? What’s not handled by this fix?
  • Add documentation of data and computations that will help future debuggers (including you).

[Tip box on technical debt] [Tip box on documentation]

6.2 Practice

6.2.1 Bug Log Entry

```Cell In[2], line 32, in check_one_product(inventory, product, request_qty) 30 message = "" 31 available_qty = inventory[product]["quantity"] —> 32 price = inventory[product]["price"] 34 if request_qty > available_qty: 35 qty = available_qty

KeyError: ‘price’```

Field Description
Symptom KeyError: ‘price’
Proximal Cause data fault: we expected the inner dictionary to have a key that it didn’t have
Root Cause
  • the Combination step should have checked for availability of a price and added one if it wasn’t in the data. OR
  • it should have been in the data in the first place.
Fixes
  • alter the processing code in check_one_product() to check for a missing price and handle it gracefully.
  • alter the processing code in create_combined_inventory() to automatically fill in a price key even if none is set.
  • preprocess the data source to ensure that the price is present.

6.2.2 Bug Recipe

Let’s try to generalize about handling key errors.

In our case, we found that the proximal cause was a data fault. But from other experience, we know that key errors often happen because the code just has the wrong expression for the dictionary or for the key. So we’ve include a code-intention mismatch as a possible proximal cause.

And we’ve tried to generalize the potential root causes and fixes, to provide a recipe for handling key errors in the future.

Bug Name KeyError
Symptoms Runtime error with message that mentions KeyError. The error message will say what key is missing. You have to figure out which dictionary is missing the key: it might be a nested dictionary inside a larger data object.
Potential Proximal Causes
  • code-intention mismatch: does the code specify the intended dictionary and the intended key?
  • data fault: the dictionary should include that key, but doesn’t.
Potential Root Causes
  • computation misunderstanding: code that should be setting the key doesn’t.
  • data fault: original data source is missing data.
Potential Fixes
  • Fix the code at the location of the proximate cause.
    • Add error checking code to gracefully handle the missing key.
  • Fix the code where the key should be added to the dictionary.
    • Correct a bug where it is not adding keys at all.
    • Add error checking code to ensure that the particular key is added.
  • Fix the data source to ensure that the key is present, possibly with a pre-processing step.

6.2.3 Future Proof Your Code

  1. Side effects. One possible side effect of all of our fixes is that we will be giving away items for free if they don’t have a price. This is a business decision that should be made by the business owner. We should check with them about whether this is the right thing to do.

  2. Technical debt. We fixed the code to gracefully handle a situation where the price of an item is missing. But what would happen if someone manages to order an item that we don’t have in the inventory? It’s not clear how that might happen, because we haven’t seen any of the code that assembles a shopping list. It might be worth adding something now to handle that situation, just in case. Or at least add some comments to the code to indicate that this is a potential problem. If we wanted to fix it now, see the code snippet below. Notice the new lines 34-35 and the new order on line 69, which includes an item that is not in the inventory. We have still left ourselves with kind of an ugly message to the shopper when the item is unavailable. We could improve that message, but we’ll leave that as technical debt for now.

  3. Documentation. We should add comments to the code that explain the changes we made. These have been added in the code below, on line 33 and 17.

Code
import csv

def read_two_column_csv(file_name):
   with open(file_name, "r") as file:
       reader = csv.reader(file)
       next(reader) # skip header row
       data = {}
       for item, value in reader:
           data[item] = value
   return data

def create_combined_inventory(quantities, prices):
   combined_data = {}
   for item in quantities:
       combined_data[item] = {
           "quantity": int(quantities[item]),
           "price": 0  # set default price, in case it's not in the prices data
       }
   for item in prices:
       price = float(prices[item].replace("$", ""))
       if item in combined_data:
           combined_data[item]["price"] = price
       else:
           combined_data[item] = {
               "price": price
           }
   return combined_data

## Process a shopping request
def check_one_product(inventory, product, request_qty):
   message = ""

   # If the product is not in the inventory, add it with zero quantity and price; TODO: improve the output that goes to the shopper
   if product not in inventory:
      inventory[product] = {"quantity": 0, "price": 0}

   available_qty = inventory[product]["quantity"]
   price = inventory[product]["price"]

   if request_qty > available_qty:
       qty = available_qty
       apology = "Sorry, we only have "
   else:
       qty = request_qty
       apology = ""
   subtotal = qty * price
   message = (
       f"${qty * price:.2f}: {apology}"
       f"{qty} {'unit' if qty == 1 else 'units'} of {product}; "
       f"${price:.2f} per unit.\n"
   )
   return subtotal, message

def process_shopping_request(shopping_request, inventory):
   response = ""
   total = 0
   for product, request_qty in shopping_request.items():
       subtotal, message = check_one_product(inventory, product, request_qty)
       total += subtotal
       response += message
   response += f"----\n${total:.2f} Total\n"
   return response

quantities = read_two_column_csv("data/quantities.csv")
prices = read_two_column_csv("data/prices.csv")

inventory = create_combined_inventory(quantities, prices)
print(process_shopping_request({"apples": 500, "bananas": 3, "pears": 1}, inventory))
print(process_shopping_request({"apples": 500, "bananas": 3, "oranges": 4, "pears": 1}, inventory))
print(process_shopping_request({"peanuts": 1, "pears": 2}, inventory))
$10.00: Sorry, we only have 10 units of apples; $1.00 per unit.
$6.00: 3 units of bananas; $2.00 per unit.
$1.50: 1 unit of pears; $1.50 per unit.
----
$17.50 Total

$10.00: Sorry, we only have 10 units of apples; $1.00 per unit.
$6.00: 3 units of bananas; $2.00 per unit.
$0.00: 4 units of oranges; $0.00 per unit.
$1.50: 1 unit of pears; $1.50 per unit.
----
$17.50 Total

$0.00: Sorry, we only have 0 units of peanuts; $0.00 per unit.
$3.00: 2 units of pears; $1.50 per unit.
----
$3.00 Total