8  OILER Example #3

Let’s go through the whole OILER process with another bug. We’ll use a slightly modified version of the code, which is exhibiting a different symptom.

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": quantities[item], "price": 0}
    for item in prices:
        price = prices[item]
        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 = ""
    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
    )
)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[1], line 63
     60 prices = read_two_column_csv("data/prices.csv")
     62 inventory = create_combined_inventory(quantities, prices)
---> 63 print(process_shopping_request({"apples": 500, "bananas": 3, "pears": 1}, inventory))
     64 print(
     65     process_shopping_request(
     66         {"apples": 500, "bananas": 3, "oranges": 4, "pears": 1}, inventory
     67     )
     68 )

Cell In[1], line 52, in process_shopping_request(shopping_request, inventory)
     50 total = 0
     51 for product, request_qty in shopping_request.items():
---> 52     subtotal, message = check_one_product(inventory, product, request_qty)
     53     total += subtotal
     54     response += message

Cell In[1], line 33, in check_one_product(inventory, product, request_qty)
     30 available_qty = inventory[product]["quantity"]
     31 price = inventory[product]["price"]
---> 33 if request_qty > available_qty:
     34     qty = available_qty
     35     apology = "Sorry, we only have "

TypeError: '>' not supported between instances of 'int' and 'str'

8.1 Orient

The code is extremely similar to the original running example, and everything from the Orient step applies here as well. As a reminder, here is the abstract execution diagram that we developed to orient ourselves.

An execution diagram for our shopping code.

8.2 Investigate

8.2.1 Describe the Symptom

Again, the program has not run to completion, and we have an error message. This time, the error message is different. It says TypeError: '>' not supported between instances of 'int' and 'str'.

8.2.2 Identify the Proximate Cause

8.2.2.1 Find the Location

Reading the stack trace error message from the bottom up, we see that the proximate location is line 35, which is the line that contains the comparison if request_qty > available_qty:.

8.2.2.2 Hypothesize a Cause

The error message is telling us that we are trying to compare an integer and a string using the > operator. So the expression to the left of the > operator, request_qty, must be an integer, and the expression to the right, available_qty, must be a string.

We can confirm this by invoking print(), as in the following. We then raise an Exception to stop the program from running further.

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": quantities[item], "price": 0}
    for item in prices:
        price = prices[item]
        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 = ""
    available_qty = inventory[product]["quantity"]
    price = inventory[product]["price"]

    print(f"type of request_qty: {type(request_qty)}")
    print(f"type of available_qty: {type(available_qty)}")
    raise Exception("Stopping execution of this cell")
    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
    )
)
type of request_qty: <class 'int'>
type of available_qty: <class 'str'>
---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
Cell In[2], line 66
     63 prices = read_two_column_csv("data/prices.csv")
     65 inventory = create_combined_inventory(quantities, prices)
---> 66 print(process_shopping_request({"apples": 500, "bananas": 3, "pears": 1}, inventory))
     67 print(
     68     process_shopping_request(
     69         {"apples": 500, "bananas": 3, "oranges": 4, "pears": 1}, inventory
     70     )
     71 )

Cell In[2], line 55, in process_shopping_request(shopping_request, inventory)
     53 total = 0
     54 for product, request_qty in shopping_request.items():
---> 55     subtotal, message = check_one_product(inventory, product, request_qty)
     56     total += subtotal
     57     response += message

Cell In[2], line 35, in check_one_product(inventory, product, request_qty)
     33 print(f"type of request_qty: {type(request_qty)}")
     34 print(f"type of available_qty: {type(available_qty)}")
---> 35 raise Exception("Stopping execution of this cell")
     36 if request_qty > available_qty:
     37     qty = available_qty

Exception: Stopping execution of this cell
8.2.2.2.1 Code vs. Intention Mismatch

Is it a code vs. intention mismatch? The intention is to compare the quantities of the requested item and the available item. The code is trying to do that. So, there is no mismatch here.

8.2.2.2.2 Computation Misunderstanding

Did we misunderstand the > operator? We thought it compared two numbers and evaluates to True if the first is larger than the second. Let’s do a quick check of our understanding.

Code
x = 5
x > 3
True
Code
x > 7
False

It doesn’t seem like we misunderstod the > operator, so let’s keep moving.

8.2.2.2.3 Data Fault

Did we expect available_qty to be a string? No, we expected it to be an integer. So, this is a data fault.

8.3 Locate the Root Cause

We could do this with either backward tracing or forward tracing. Let’s do forward tracing this time, since we did backward tracing last time and it turned out that forward tracing would have been easier.

After we read in the files, we would expect the quantities to be integers. So, let’s call print() to check whether they are.

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": quantities[item], "price": 0}
    for item in prices:
        price = prices[item]
        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 = ""
    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")
print(f"type of quantities['apples']: {type(quantities['apples'])}")

# 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))
type of quantities['apples']: <class 'str'>

Hmm. It’s a string. So, the root cause is that we misunderstood the computation defined in read_two_column_csv(). It is reading the values as strings instead of as numbers.

8.3.0.1 Experiment with Fixes

We will need to convert those strings into numbers. We can do this by using the int() function.

But when should we do that?

We could alter the read_two_column_csv(), to make it match our expectations of what that function was supposed to do. But we need to be careful. That function is also used to read in prices, and could be used to read in any two-column CSV file.

So, we could add an optional parameter that specifies that we want an integer conversion.

Code
import csv


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


def create_combined_inventory(quantities, prices):
    combined_data = {}
    for item in quantities:
        combined_data[item] = {"quantity": quantities[item], "price": 0}
    for item in prices:
        price = prices[item]
        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 = ""
    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", convert_to_integer=True)
prices = read_two_column_csv("data/prices.csv")
print(f"type of quantities['apples']: {type(quantities['apples'])}")

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
    )
)
type of quantities['apples']: <class 'int'>
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[6], line 68
     65 print(f"type of quantities['apples']: {type(quantities['apples'])}")
     67 inventory = create_combined_inventory(quantities, prices)
---> 68 print(process_shopping_request({"apples": 500, "bananas": 3, "pears": 1}, inventory))
     69 print(
     70     process_shopping_request(
     71         {"apples": 500, "bananas": 3, "oranges": 4, "pears": 1}, inventory
     72     )
     73 )

Cell In[6], line 56, in process_shopping_request(shopping_request, inventory)
     54 total = 0
     55 for product, request_qty in shopping_request.items():
---> 56     subtotal, message = check_one_product(inventory, product, request_qty)
     57     total += subtotal
     58     response += message

Cell In[6], line 45, in check_one_product(inventory, product, request_qty)
     42     apology = ""
     43 subtotal = qty * price
     44 message = (
---> 45     f"${qty * price:.2f}: {apology}"
     46     f"{qty} {'unit' if qty == 1 else 'units'} of {product}; "
     47     f"${price:.2f} per unit.\n"
     48 )
     49 return subtotal, message

ValueError: Unknown format code 'f' for object of type 'str'

Well, that conversion successfully made it so that quantities['apples'] is an integer. But eliminating that TypeError has triggered a ValueError somewhere later in the exeuction. We will have to go through the OILER process for that once we are done with the process for the TypeError we’ve been working on.

Another option for a fix is to convert the strings to numbers as part of the Combine step. That’s what the original code in the running example did.

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])}
    for item in prices:
        price = prices[item]
        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 = ""
    available_qty = inventory[product]["quantity"]
    if "price" in inventory[product]:
        price = inventory[product]["price"]
    else:
        price = 0

    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
    )
)

That also resolved the initial bug, but triggers the same follow-on error.

We could even move the conversion to the shopping part of the code, where we are actually using the quantities.

See line 35 below. This would be a little less efficient, since we would be converting the strings to numbers every time we use them. And if we use them somewhere else in the code, we would have to do the conversion there as well. Thus, it would be better to do the conversion in the Combine step or in the read_two_column_csv function.

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": quantities[item], "price": 0}
    for item in prices:
        price = prices[item]
        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 = ""
    available_qty = inventory[product]["quantity"]
    price = inventory[product]["price"]

    available_qty = int(available_qty)

    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
    )
)

8.4 Reflect

8.4.1 Bug Log Entry

Cell In[1], line 35
     32 available_qty = inventory[product]["quantity"]
     33 price = inventory[product]["price"]
---> 35 if request_qty > available_qty:
     36     qty = available_qty
     37     apology = "Sorry, we only have "

TypeError: '>' not supported between instances of 'int' and 'str'
Field Description
Symptom TypeError: ‘>’ not supported between instances of ‘int’ and ‘str’
Proximal Cause data fault: we expected the inventory quantities to be integers but they were strings
Root Cause computation misunderstanding: in the Read step of the program it read the quantities as strings instead of as integers
Fix Convert the string to integers, either in the Read step, the Combine step, or the Shop step

8.4.2 Bug Recipe

Generalizing, let’s write a recipe for diagnosing and fixing similar bugs in the future.

Bug Name TypeError
Symptoms Runtime error with message that mentions TypeError. The error message says operands that are not of compatible types. You have to figure out which variable names or expressions correspond to the types it mentions.
Potential Proximal Causes
  • code-intention mismatch: was that the operator you intended? the two operands?
  • computation misunderstanding: does the operator work the way you thought?
  • data fault: are the data values what you expected them to be?
Potential Root Causes
  • computation misunderstanding: code that is reading and setting the data is not converting it into the type you expected. In particular, remember that the csv module always reads in data as strings.
Potential Fixes If the data is correct but just needs a data type conversion, decide the best place to do the conversion:
  • at the location where data is read in from a file or other external source.
  • somewhere during intermediate processing.
  • at the location of use. But this may be less desirable, as it requires redundant conversions if the data is used multiple times or in multiple places in the code.

8.5 Future Proof Your Code

8.5.1 Side Effects

There is a follow-on error that we haven’t really looked at. Is there any chance that converting the quantities from strings to integers could have caused that problem, or any other problem? Let’s have a quick look at the last part of that stack trace:

Cell In[28], line 45
     42     apology = ""
     43 subtotal = qty * price
     44 message = (
---> 45     f"${qty * price:.2f}: {apology}"
     46     f"{qty} {'unit' if qty == 1 else 'units'} of {product}; "
     47     f"${price:.2f} per unit.\n"
     48 )
     49 return subtotal, message

ValueError: Unknown format code 'f' for object of type 'str'

In this error message, the f is referring to price:.2f, not the ff before the quotes that signals the start of a formatted string literal (f-string). This f is a format specifier that is used to format a number as a floating point number. So, it seems that the error message is telling us that the price variable is a string, not a number.

This is not likely to be a side effect of our turning the quantities into integers, so the fix we made is OK. But it is a clue about what the next bug is.

8.5.2 Identify Technical Debt

When we converted the quantities to integers, we might have noticed that we have a similar problem with the prices. They are also strings, not numbers. But we probably do want them to be numbers, in this case floating point numbers. The error message that we just analyzed suggests this as well.

We have identified some technical debt in the program. At some point, the fact that we are reading in prices as strings will cause a problem. And actually, that debt is already coming due, in the form of our next error.

So let’s make a fix for that now.

We could add another special-case flag in read_two_column_csv to handle converting the price strings to floating point numbers.

But let’s do something a little more elegant. Let’s make the optional parameter be a converter function that converts the second column string in any arbitrary way. This way, the read_two_column_csv function is even more general and reusable.

We will make the default converter function be the identity function, so that it doesn’t change the value at all. This way, we can use the function as we have been using it, without having to specify the converter function. But we can also use it to convert the prices to floating point numbers and the quantities to integers.

See lines 5, 11, and 61-66 below.

Code
import csv


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


def create_combined_inventory(quantities, prices):
    combined_data = {}
    for item in quantities:
        combined_data[item] = {"quantity": quantities[item], "price": 0}
    for item in prices:
        price = prices[item]
        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 = ""
    available_qty = inventory[product]["quantity"]
    price = inventory[product]["price"]
    print(f"type of price: {type(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", int
)  # int acts as a built-in function, converting a string to an integer
prices = read_two_column_csv(
    "data/prices.csv", lambda x: float(x[1:])
)  # remove the dollar sign and convert to a float
print(
    f"types of quantities and prices: {type(quantities["apples"])}, {type(prices["apples"])}"
)

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
    )
)
types of quantities and prices: <class 'int'>, <class 'float'>
type of price: <class 'float'>
type of price: <class 'float'>
type of price: <class 'float'>
$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

type of price: <class 'float'>
type of price: <class 'float'>
type of price: <class 'int'>
type of price: <class 'float'>
$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

Paying down this technical debt has yielded a nice bonus. We got rid of the next bug as well.

8.5.3 Documentation

We could make sure future programmers understand what the combine function is doing by adding type hints and a docstring. This will primarily help someone in the Orient stage of debugging, but it could also help in identifying a data fault or a computation misunderstanding.

Code
import csv


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


def create_combined_inventory(
    quantities: dict[str, int], prices: dict[str, float]
) -> dict[str, dict]:
    """
    Create a combined inventory dictionary from quantities and prices.

    Parameters:
       quantities (dict[str, int]): A dictionary where keys are item names and values are quantities.
       prices (dict[str, float]): A dictionary where keys are item names and values are prices.

    Returns:
       dict[str, dict]: A dictionary where keys are item names and values are dictionaries containing
                        'quantity' and 'price' keys.
    """

    combined_data = {}
    for item in quantities:
        combined_data[item] = {"quantity": quantities[item], "price": 0}
    for item in prices:
        price = prices[item]
        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 = ""
    available_qty = inventory[product]["quantity"]
    price = inventory[product]["price"]
    print(f"type of price: {type(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", int
)  # int acts as a built-in function, converting a string to an integer
prices = read_two_column_csv(
    "data/prices.csv", lambda x: float(x[1:])
)  # remove the dollar sign and convert to a float
print(
    f"types of quantities and prices: {type(quantities["apples"])}, {type(prices["apples"])}"
)

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
    )
)
types of quantities and prices: <class 'int'>, <class 'float'>
type of price: <class 'float'>
type of price: <class 'float'>
type of price: <class 'float'>
$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

type of price: <class 'float'>
type of price: <class 'float'>
type of price: <class 'int'>
type of price: <class 'float'>
$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