Your tracker from Part 11 loads fine. Then a user types the word lunch where a number should go, and the whole program falls over with a wall of red text.
raw = 'lunch'
amount = float(raw)
print('you spent', amount)Traceback (most recent call last):
File "tracker.py", line 2, in <module>
amount = float(raw)
^^^^^^^^^^
ValueError: could not convert string to
float: 'lunch'That block is a traceback, and it is not the program yelling at you. It is Python handing you a report of what went wrong and where. This part is about reading that report, then writing code that keeps going instead of crashing when the input is bad. By the end your expense tracker refuses a bad amount politely and stays alive.
try block for the risky line and an except block for the recovery. Catch the specific error you expect, not every error. Read every traceback from the last line up.This part assumes Part 11, where your tracker learned to save and load a CSV file whose amounts come back as text. You need Python 3 and about twenty five minutes. Every snippet runs as printed, and you can copy it and run it as is. One reminder that bites people copying code off a page: straight quotes can turn into curly quotes in transit, and Python rejects a curly quote with a SyntaxError. If a pasted line breaks on the quotes, retype them.
Read the traceback from the bottom up
A traceback looks like a lot at first. The trick is that the useful line is the last one, and you read upward only if you need more. The last line names the type of error and gives a plain message. Everything above it is the trail of calls that led there.
Figure 1. Start at the red line at the bottom. It names the error type and the reason.
In the opener, the last line says ValueError and tells you the reason: it could not convert 'lunch' to a float. The line above points at the exact statement, amount = float(raw), and the carets underline the part that failed. You now know what broke and where, which is most of debugging.
Different mistakes raise different error types, and the type is a strong hint. Here are the handful you will meet in your first months.
| Error type | You get it when | Typical message |
|---|---|---|
ValueError | Right type, wrong value | could not convert string to float |
TypeError | Wrong type for the operation | can only concatenate str to str |
KeyError | A dict key is missing | ‘category’ |
IndexError | A list position is out of range | list index out of range |
ZeroDivisionError | You divide by zero | division by zero |
FileNotFoundError | A file is not where you looked | No such file or directory |
Table 1. The exceptions a beginner meets earliest, and how to recognise them.
try and except: keep going after a bad value
You wrap the risky line in a try block. If it raises an exception, Python jumps straight to the matching except block instead of crashing. Name the specific error you expect after the except keyword.
raw = 'lunch'
try:
amount = float(raw)
print('you spent', amount)
except ValueError:
print('that is not a number')that is not a number
The program did not stop. It tried the conversion, saw the ValueError, ran the recovery line, and carried on. Feed the same code a real number and the try block succeeds, so the except block never runs.
raw = '120.5'
try:
amount = float(raw)
print('you spent', amount)
except ValueError:
print('that is not a number')you spent 120.5
Grab the message when you need it
Sometimes you want the actual message Python produced, to log it or show it. Add as err and you get the exception object, which prints as its message.
raw = 'ten'
try:
amount = int(raw)
except ValueError as err:
print('could not read amount:', err)could not read amount: invalid literal for int() with base 10: 'ten'
That is the same message you saw in the raw traceback, now under your control instead of crashing the program.
Catch the error you expect, not every error
You can write except with no error type at all. It catches everything. Beginners love it because it makes the red text go away. It is also one of the worst habits you can pick up, so I want to name it early.
try:
amount = float(raw)
except:
amount = 0A bare except swallows every problem, including the ones you did not plan for. If you typo a variable name inside the try block, that is a NameError, a real bug, and the bare except hides it by quietly setting amount to zero. You get no traceback and no clue. The program limps along with wrong data.
ValueError. Let anything else you did not expect blow up loudly, because a loud crash with a traceback is a gift. It tells you exactly what to fix. A silent wrong answer tells you nothing until much later, when it is far harder to trace.This runs against the beginner instinct to make errors disappear. The goal is not zero red text. The goal is that the only errors you hide are the ones you chose to handle, and every other bug still shouts.
When two different problems need two different responses, stack the except blocks. Python checks them top to bottom and runs the first that matches.
data = {'amount': '0'}
try:
amt = float(data['amount'])
share = 100 / amt
except ValueError:
print('amount was not a number')
except ZeroDivisionError:
print('amount was zero')amount was zero
else and finally: the two blocks people forget
A full try statement has four parts. You will use the first two constantly and the other two now and then, but knowing all four keeps your code tidy.
| Block | Runs when | Use it for |
|---|---|---|
try | always, first | the one risky line |
except | an error was raised | the recovery |
else | no error was raised | work that needs the try to have worked |
finally | always, last | cleanup, run either way |
Table 2. The four blocks of a try statement and when each one fires.
Figure 2. On an error Python runs except. With no error it runs else. Either way finally runs last.
Here they are together. The else holds the follow up work that only makes sense if the conversion worked, and the finally runs no matter what.
raw = '45'
try:
amount = float(raw)
except ValueError:
print('bad amount, skipping')
else:
print('recording', amount)
finally:
print('done with this entry')recording 45.0 done with this entry
Keeping the success work in else rather than piling it into the try block has a real payoff: the try stays down to the single line that might fail, so your except catches only that line and cannot accidentally swallow an error from the follow up work.
Raising your own error
Catching is half the story. Sometimes your code is the thing that should refuse. A negative expense is nonsense, and the honest move is to raise a ValueError yourself with a message that says why.
def check_amount(amount):
if amount < 0:
raise ValueError('amount cannot be negative')
return amount
print(check_amount(50))
print(check_amount(-5))50
Traceback (most recent call last):
File "check.py", line 7, in <module>
print(check_amount(-5))
^^^^^^^^^^^^^^^^^
File "check.py", line 3, in check_amount
raise ValueError('amount cannot be negative')
ValueError: amount cannot be negativeThe caller can now wrap that in a try and decide what to do. Raising a clear error at the source beats returning a fake value like -1 that some later line has to remember to check. Fail where the problem is, with a message that names it.
add_expense that reads a typed amount and rejects anything that is not a positive number, without ever crashing. It builds on the save_expenses and load_expenses from Part 11.
def read_amount(raw):
try:
amount = float(raw)
except ValueError:
return None
if amount <= 0:
return None
return amount
def add_expense(items, raw, category):
amount = read_amount(raw)
if amount is None:
print('skipped, not a valid amount')
return items
items.append(
{'amount': amount,
'category': category})
print('added', amount, category)
return items
expenses = []
expenses = add_expense(expenses, 'lunch', 'food')
expenses = add_expense(expenses, '120.5', 'food')
print('kept', len(expenses), 'expenses')skipped, not a valid amount added 120.5 food kept 1 expenses
except that catches everything?" The answer they want: it hides errors you never meant to handle, including real bugs like a NameError or a typo, so the program keeps running with wrong data and no traceback to point you at the cause. A strong follow up is that you should catch the narrowest exception that fits, and let the rest propagate. It is a quick tell for whether you have debugged real code or only made errors disappear.safe_divide(a, b) that returns a / b, but returns the word undefined if b is zero, without crashing. Test it with safe_divide(10, 2) and safe_divide(10, 0). Expected output:
5.0 undefined
Show solution
def safe_divide(a, b):
try:
return a / b
except ZeroDivisionError:
return 'undefined'
print(safe_divide(10, 2))
print(safe_divide(10, 0))The division sits in the try. Only a zero divisor raises ZeroDivisionError, so that is the one exception you catch, and everything else would still surface normally.
Cleaning the amounts you loaded
Part 11 left a loose thread. When you read the CSV back, every amount arrives as text, and one bad row should not sink the whole load. Loop over the rows, try to convert each amount, and set aside the ones that will not parse instead of letting the first bad value stop everything.
rows = [
{'amount': '120.5', 'category': 'food'},
{'amount': '', 'category': 'travel'},
{'amount': '40', 'category': 'books'},
]
good = []
bad = 0
for row in rows:
try:
row['amount'] = float(row['amount'])
except ValueError:
bad = bad + 1
continue
good.append(row)
print('loaded', len(good), 'rows')
print('skipped', bad, 'bad rows')loaded 2 rows skipped 1 bad rows
The empty amount is counted and skipped, while the clean rows come through as real numbers ready for maths. The continue jumps to the next row the moment a conversion fails, so nothing broken slips into good. This is the everyday shape of loading real data: most of it is fine, a little of it is not, and your job is to keep going and keep a count.
A bug, start to finish
Let me walk one real failure the way you would at your desk. You load expenses and print each category. It crashes on the third row.
rows = [
{'amount': '120.5', 'category': 'food'},
{'amount': '40', 'category': 'travel'},
{'amount': '55'},
]
def show(row):
print(row['category'], row['amount'])
for row in rows:
show(row)food 120.5
travel 40
Traceback (most recent call last):
File "t.py", line 12, in <module>
show(row)
^^^^^^^^^
File "t.py", line 9, in show
print(row['category'], row['amount'])
~~~^^^^^^^^^^^^
KeyError: 'category'Read the last line first. It is a KeyError, and the key it names is 'category'. So a row is missing that key. The line above shows the failing statement is inside show, and the line above that shows show was called from the loop. The trail runs from where you called down to where it broke.
Figure 3. An uncaught exception rises through each call until it reaches the top and stops the program.
The fix depends on intent. If every row should have a category, the data is wrong and you want to know, so leave it to crash while you fix the file. If a missing category is allowed, handle it with .get(), which returns a default instead of raising:
def show(row):
cat = row.get('category', 'uncategorised')
print(cat, row['amount'])food 120.5 travel 40 uncategorised 55
Notice the choice. You did not reach for a try here because a dictionary already offers .get() with a default, which is clearer than catching a KeyError. Handling errors well is partly knowing when a plain tool avoids the error in the first place.
What beginners ask when code breaks
Is an exception the same as a syntax error? No. A SyntaxError means Python could not even read your code, so nothing runs and you cannot catch it with try. An exception happens while a valid program runs, and that is the kind you handle.
Which exception should I catch if I am not sure? Start by letting it crash once and read the last line of the traceback. It names the exact type. Catch that type. Guessing leads to catching too much.
Can one except block catch several error types? Yes. Group them in parentheses, as in except (ValueError, TypeError):, when the recovery for both is the same.
Should I check the value first or just try it? In Python the common style is to try the operation and catch the failure, rather than writing a pile of if checks first. It reads better and avoids gaps where your checks miss a case. Reach for try before a wall of validation.
Why does my except block never run? Usually the error you are catching is not the one being raised. Print the traceback, read its last line, and match the type exactly. Catching ValueError will not catch a KeyError.
try and recover in except, catch the specific error instead of hiding every problem, use else and finally, and raise your own ValueError when input is wrong. Your tracker refuses bad amounts and keeps running.Everything so far has lived in one file. Next comes the standard library: the batteries Python ships with, from dates to random numbers, and how to pull them in with import. Bring your expenses.py to Part 13.
References
Python docs: Errors and Exceptions
Python docs: The try statement
Python docs: Built-in exceptions


DrJha