Traceback (most recent call last):
File "basket.py", line 16, in <module>
print(basket_total(["£4.50", "£1.20", "3.99"]))
~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "basket.py", line 12, in basket_total
total += parse_price(text)
TypeError: unsupported operand type(s) for +=: 'float' and 'NoneType'A traceback is read from the bottom. This small one has nothing to do with prices. It shows the same shape of mistake, so you can see the pattern before you meet it in the exercise.
def initial(name):
if name:
return name[0].upper()
# an empty name falls off the end here, and returns None
names = ["ada", "", "grace"]
letters = [initial(n) for n in names]
print(letters) # -> ['A', None, 'G']
print("".join(letters))
# TypeError: sequence item 1: expected str instance, NoneType found
- The last line names the problem
"NoneType found" is the clue. Somewhere a value that should be text is None, and in Python a None you did not write is almost always a function that ended without a return.
- The line it points at is where it was noticed
The crash is in join, but join did nothing wrong: it was handed a list with a None in it. The bug is two lines up, in the function that made the None.
- Ask which input took the other path
Item 1 is the empty name. Follow that one input through initial and you arrive at the path with no return. That is the whole method: find the input, follow it, find the path.
The traceback says total and something that is None were added. total is a number, so the None is what parse_price returned. For which of the three prices?
"3.99" does not start with a pound sign. Follow it through parse_price: the if is false, and the function reaches its end without a return.
Give parse_price a return on every path. The simplest version removes the pound sign if there is one, then converts whatever is left, on one line.
def parse_price(text): text = text.strip() if text.startswith("£"): text = text[____:] return ____(text)
def parse_price(text):
"""'£4.50' or '4.50' -> 4.5"""
return float(text.strip().removeprefix("£"))
def basket_total(prices):
"""Add up a list of price strings, to the penny."""
total = 0
for text in prices:
total += parse_price(text)
return round(total, 2)
basket_total is unchanged. The traceback pointed at the addition because that is where the None finally met something that could not handle it; the bug was the missing return in the other function.
removeprefix does nothing to text without a pound sign, so there is no longer an if with a branch that can forget to return. The mistake is not fixed so much as made impossible.
float("free") raises ValueError, which is exactly what the contract asks for. Catching it and returning 0 would make a broken spreadsheet look like a smaller bill.
Floats are fine for a checkout demo and wrong for real money: add enough of them and the rounding at the end stops being enough. Money that has to reconcile is kept in pence as whole numbers, or in decimal.Decimal, from the moment it is parsed.
- Accept prices in pence too, like "450p", without adding a second if that can forget to return.
- Make the ValueError say which price in the basket was not a price, and its position.
- Rewrite it with decimal.Decimal and explain why the round at the end is no longer needed.