, ,

Reading Tracebacks and Handling Exceptions (Python for Beginners, Part 12)

A beginner friendly guide to Python errors: read a traceback from the bottom up, catch the right exception with try and except, and stop bad input from crashing your program.

Python for Beginners · Part 12 of 20

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.

python
raw = 'lunch'
amount = float(raw)
print('you spent', amount)
output
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.

Start hereWhen Python hits a problem it cannot solve, it raises an exception and stops. You catch it with a 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.

Traceback (most recent call last): File tracker.py, line 2 amount = float(raw) ValueError: could not convert … read this line first

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 typeYou get it whenTypical message
ValueErrorRight type, wrong valuecould not convert string to float
TypeErrorWrong type for the operationcan only concatenate str to str
KeyErrorA dict key is missing‘category’
IndexErrorA list position is out of rangelist index out of range
ZeroDivisionErrorYou divide by zerodivision by zero
FileNotFoundErrorA file is not where you lookedNo 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.

python
raw = 'lunch'
try:
    amount = float(raw)
    print('you spent', amount)
except ValueError:
    print('that is not a number')
output
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.

python
raw = '120.5'
try:
    amount = float(raw)
    print('you spent', amount)
except ValueError:
    print('that is not a number')
output
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.

python
raw = 'ten'
try:
    amount = int(raw)
except ValueError as err:
    print('could not read amount:', err)
output
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.

do not do this
try:
    amount = float(raw)
except:
    amount = 0

A 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.

The habit that saves you hoursCatch the narrowest exception that fits. If you are converting text to a number, catch 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.

python
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')
output
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.

BlockRuns whenUse it for
tryalways, firstthe one risky line
exceptan error was raisedthe recovery
elseno error was raisedwork that needs the try to have worked
finallyalways, lastcleanup, run either way

Table 2. The four blocks of a try statement and when each one fires.

try error? except: recover else: continue yes no finally: always

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.

python
raw = '45'
try:
    amount = float(raw)
except ValueError:
    print('bad amount, skipping')
else:
    print('recording', amount)
finally:
    print('done with this entry')
output
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.

python
def check_amount(amount):
    if amount < 0:
        raise ValueError('amount cannot be negative')
    return amount

print(check_amount(50))
print(check_amount(-5))
output
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 negative

The 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.

Project step: the tracker refuses a bad amountGive your tracker an 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.
expenses.py
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')
output
skipped, not a valid amount
added 120.5 food
kept 1 expenses
The bad word was refused, the real number was kept, and the program stayed alive the whole time. That is the difference between a script and a tool someone else can use.
Why this matters on the jobReal input is messy. Users mistype, files have blank rows, an API returns a field you did not expect. Code that assumes clean input works in a demo and pages someone at 2am in production. The engineer who wraps the risky line, catches the specific error, and logs a clear message writes code the team trusts to run unattended. This is one of the first things a reviewer looks for, and doing it by reflex marks you as someone who has shipped before.
Real interview question"What is wrong with using a bare 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.
Try it yourselfWrite a function 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:
expected output
5.0
undefined
Show solution
python
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.

python
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')
output
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.

python
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)
output
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.

the for loop show(row) the error starts here and travels up to the caller

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:

python
def show(row):
    cat = row.get('category', 'uncategorised')
    print(cat, row['amount'])
output
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.

You can nowYou can now read a traceback from its last line, wrap a risky line in 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.

Python for Beginners · Part 12 of 20
« Previous: Part 11  |  Complete Guide  |  Next: Part 13

References

Python docs: Errors and Exceptions
Python docs: The try statement
Python docs: Built-in exceptions

About The Author


Discover more from Journal of Intelligent Infrastructure – By Dr Pranay Jha

Subscribe to get the latest posts sent to your email.

Leave a Reply

Your email address will not be published. Required fields are marked *

Architect’s Toolkit

About the Author

Dr. Pranay Jha is a Cloud and AI Consultant with 18+ years of experience in hybrid cloud, virtualization, and enterprise infrastructure transformation. He specializes in VMware technologies, multi-cloud strategy, and Generative AI solutions. He holds a PhD in Computer Applications with research focused on Cloud and AI, has published multiple research papers, and has been a VMware vExpert since 2016 and a VMUG Community Leader.

Discover more from Journal of Intelligent Infrastructure - By Dr Pranay Jha

Subscribe now to keep reading and get access to the full archive.

Continue reading