try:
score = int('n/a')
except ValueError:
score = None
print(score)None
Before this lesson
P01.8.1 Reading a traceback P01.7.4 Tables in files: CSV and JSON
After this lesson you can
In review. The maths and code on this page are checked by automated tests on every change, but an independent reader has not signed it off yet. Spot a mistake or a confusing step? Tell us.
Two new lines have crept into the study file: Femi,3,n/a and Gus,4. The hand-made parser of P01.7.4 stops at the first of them with a traceback, and the five good rows are lost with it. Real data files always have lines like these. How does a program step over the bad lines, count them and keep the good ones, without also stepping over mistakes nobody expected?
From the last lesson
1. Which line of a traceback do you read first, and what does it give you?
The last line: the type of the exception, a colon, and a message with the details.
2. What does “most recent call last” say about the order of the entries?
They are listed oldest first, so the frame where the error happened is the last entry, just above the error line.
An exception does not stop the program at once. It leaves the frame where it was raised and goes to the caller, then to the caller’s caller, until it reaches the global frame. Only then, with nowhere left to go, does Python print the traceback and stop.
A try statement is a net placed under a block of code on the way. If a line inside the block raises an exception of a kind the net names, the exception is caught there: the rest of the block is skipped, a block of your choosing runs instead, and the program carries on.
The try statement as a flow chart. Green arrows are the normal path, purple ones carry an exception. Every path passes through the finally block.
Read the chart from the top. If no line raises, the else block runs. If a line raises, the rest of the try block is skipped and the except clauses are asked, one by one, whether they name its type. If one does, its block runs and the exception is gone. If none does, the exception carries on up to the caller, exactly as if there had been no try. Either way, the finally block runs last.
Step 1. The crash. Here is the parser of P01.7.4 as two functions, run on the dirty file.
load.py
def parse_line(line):
parts = line.split(',')
row = {'name': parts[0]}
row['hours'] = int(parts[1])
row['score'] = int(parts[2])
return row
def load_rows(text):
rows = []
for line in text.splitlines()[1:]:
rows.append(parse_line(line))
return rows
with open('study_dirty.csv', encoding='utf-8') as file:
text = file.read()
rows = load_rows(text)
print(len(rows), 'rows')Traceback (most recent call last):
File "load.py", line 18, in <module>
rows = load_rows(text)
File "load.py", line 12, in load_rows
rows.append(parse_line(line))
~~~~~~~~~~^^^^^^
File "load.py", line 5, in parse_line
row['score'] = int(parts[2])
~~~^^^^^^^^^^
ValueError: invalid literal for int() with base 10: 'n/a'(Copied from Python 3.15, with the folder cut from the file name; other versions place the marks ^ and ~ a little differently.) Femi’s score 'n/a' cannot become a whole number. The exception leaves parse_line, then load_rows, then the global frame, and the program stops. The list rows already held Ama and Ben, but it was a local name of load_rows, so it is lost too.
Step 2. Catch one exception. Put the risky line in a try block and name the exception you expect after except.
Step 3. Keep the exception. except ValueError as error binds the name error to the exception that was caught, so that you can print its message.
could not read the score: invalid literal for int() with base 10: 'n/a'
Step 4. Where the exception is caught. A try in one frame catches exceptions raised in any call made from inside its block, however deep. The frames between are thrown away, with every name in them.
An exception climbs the call stack until a frame with a matching except catches it. Frames below that one are thrown away; the frame that caught it carries on.
Step 5. else and finally. An else block runs only when the try block raised nothing. A finally block runs last, whatever happened.
else: no exception
finally: always runs
55
except: not a whole number
finally: always runs
None
Step 6. Raise your own. In P01.8.1, mean([]) stopped with division by zero, deep inside the arithmetic. A function can check its input first and stop with a message that says what is actually wrong. The statement raise ValueError('...') makes the exception and raises it.
The rules, from the Python Tutorial (8.3, 8.7) and the Language Reference (8.4):
An except clause that names a type also catches its kinds, the types listed under it in the table of built-in exceptions: except LookupError catches both IndexError and KeyError, and except ArithmeticError catches ZeroDivisionError. A bare except, written except: with no type at all, catches every exception there is.
To raise an exception yourself, write raise and the exception:
The type should say what kind of problem it is (ValueError for a value a function cannot use) and the message should say what was wrong. Python 3.14 and later also accept except ValueError, IndexError: without brackets when there is no as; with the brackets your code runs on every version.
Now the loader for the dirty file. The try goes around the one call that can fail, inside the loop, so that a bad line is skipped and the loop goes on. It catches the two kinds of failure the file can produce and nothing else.
def parse_line(line):
parts = line.split(',')
row = {'name': parts[0]}
row['hours'] = int(parts[1])
row['score'] = int(parts[2])
return row
def load_rows(text):
rows = []
skipped = 0
lines = text.splitlines()
for number, line in enumerate(lines[1:], start=2): # file lines counted from 1
try:
row = parse_line(line)
except (ValueError, IndexError) as error:
print(f'line {number} skipped: {error}')
skipped += 1
else:
rows.append(row)
return rows, skippedTrace it by hand over the seven lines that follow the header.
The loader on the dirty file, one row per line. The two bad lines raise different exceptions; both are caught, counted and skipped, and the loop goes on.
Femi,3,n/a. parts is ['Femi', '3', 'n/a']. The hours convert, then int('n/a') raises ValueError. The rest of the try block, here nothing more, is skipped; the first except names ValueError, so it prints the message and adds 1 to skipped. The else block does not run, so no row is added.Gus,4. parts is ['Gus', '4'], only two items, at positions 0 and 1. So parts[2] raises IndexError before int is even called, and the same except catches it.After the last line, rows holds the five students and skipped is 2. The mean of their scores is \({300 / 5} = 60.0\), the number from every earlier lesson.
Run the loader on the dirty file, written to a temporary folder.
import tempfile
from pathlib import Path
DIRTY = '''name,hours,score
Ama,2,55
Ben,4,65
Femi,3,n/a
Chen,5,80
Dina,1,40
Gus,4
Eli,3,60
'''
with tempfile.TemporaryDirectory() as folder:
path = Path(folder, 'study_dirty.csv')
path.write_text(DIRTY, encoding='utf-8')
with open(path, encoding='utf-8') as file:
rows, skipped = load_rows(file.read())
print(len(rows), 'rows kept,', skipped, 'skipped')
scores = []
for row in rows:
scores.append(row['score'])
print('mean of the kept scores:', mean(scores))line 4 skipped: invalid literal for int() with base 10: 'n/a'
line 7 skipped: list index out of range
5 rows kept, 2 skipped
mean of the kept scores: 60.0
The library version reads the same file with csv.DictReader (P01.7.4). Watch what happens to Gus: the reader fills a missing field with None, so the short line no longer raises IndexError. It is int(None) that fails, with a TypeError. The same bad line, a different exception: always check which exceptions the code you call really raises.
import csv
def load_rows_csv(path):
rows = []
skipped = 0
with open(path, encoding='utf-8', newline='') as file: # newline='' as the csv docs ask
for row in csv.DictReader(file):
try:
row['hours'] = int(row['hours'])
row['score'] = int(row['score'])
except (ValueError, TypeError) as error:
print('skipped', row['name'] + ':', error)
skipped += 1
else:
rows.append(row)
return rows, skipped
with tempfile.TemporaryDirectory() as folder:
path = Path(folder, 'study_dirty.csv')
path.write_text(DIRTY, encoding='utf-8')
rows_csv, skipped_csv = load_rows_csv(path)
print(len(rows_csv), 'rows kept,', skipped_csv, 'skipped')skipped Femi: invalid literal for int() with base 10: 'n/a'
skipped Gus: int() argument must be a string, a bytes-like object or a real number, not 'NoneType'
5 rows kept, 2 skipped
The traceback explorer of this module lives in the previous lesson. Go back to it and turn on its switch, which wraps the call in try and except. Run the three bad inputs again: the exception climbs from the failing frame to the try in the global frame, the frames in between are thrown away, and the program prints a line instead of a traceback.
Wrong beside right. One try around the whole loop stops reading at the first bad line. A fresh try for every line skips only the bad ones.
“One try around everything is safest.”
A try catches the exception, but the rest of its block is skipped. Around the whole loop, that means every line after Femi: only 2 rows are kept. Put the try around the one call that can fail, as close to it as you can.
Wrong beside right. A bare except swallows a typo along with the bad data. Naming the exceptions you expect lets the typo stop the program with a traceback that points at it.
“A bare except catches everything, so nothing can go wrong.”
That is the trouble: it catches your own mistakes too. With a typo, parse_line(lne), every line raises NameError, the bare except counts all seven as bad data, and the program quietly reports 0 rows. Name only the exceptions you expect; anything else should stop the program and say where. except Exception: is almost as bad.
“Return None, or 0, instead of raising.”
A function that returns 0 for the mean of an empty list gives a number that looks real and travels on into every later sum. MIT’s 6.100L calls failing silently “a bad idea”. Raise an exception with a clear message, and let the caller decide what to do.
1. When does the else block of a try statement run?
Only when the try block raised no exception.
2. When does the finally block run?
Last, on every path: after the try block, after a handler, and also when an exception is about to go on to the caller.
3. What happens to an exception that no except clause matches?
It goes on to the caller, as if there were no try. If no frame catches it, the program stops with a traceback.
4. Why is a bare except: a bad idea?
It catches every exception, including the ones caused by real bugs such as a typo, so those bugs are hidden instead of reported.
5. Which two exceptions do the bad lines Femi,3,n/a and Gus,4 raise in parse_line?
ValueError from int('n/a'), and IndexError from parts[2], because 'Gus,4' splits into only two parts.
1. ★ What does to_int('7') print and return? And to_int('seven')?
int('7') works, so the except block is skipped, the else block prints else: no exception, the finally block prints finally: always runs, and it returns 7. For 'seven', int raises ValueError: the except block prints except: not a whole number and sets value to None, the finally block prints its line, and it returns None.
2. ★★ Write mean_hours(rows) that returns the mean of the 'hours' of the rows and raises ValueError('no rows') when rows is empty. Then call it inside try and except so that an empty list prints no rows instead of a traceback.
It prints no rows. On the five good rows it returns \({15 / 5} = 3.0\), the mean hours of the study table.
3. ★★★ A third bad line is added to the dirty file: Hana,two,70. What does the loader of the worked example print and return now? Then a fourth: Ivo,5,90,extra. Is it caught, and should it be?
Hana,two,70 splits into three parts, and int('two') raises ValueError at the hours, so it is caught and skipped: the loader prints line 9 skipped: invalid literal for int() with base 10: 'two' (if Hana is added as line 9) and returns the 5 good rows with skipped = 3.
Ivo,5,90,extra splits into four parts. parse_line only looks at the first three, so nothing is raised and the row is kept with score 90. The loader neither catches nor sees the problem. If an extra field means the line is broken, parse_line should check len(parts) != 3 and raise a ValueError with a message such as 'expected 3 fields, got 4', so that the loader can skip it like the others.
Catch only what you expect, as close to the risky call as you can; raise clear errors of your own.
The try statement, path by path.
Sources. Each was opened and checked against this lesson on 12 November 2026.
newline='').as, the brackets are still required.Next. P01.8.3 Debugging as a method finds the mistakes that raise nothing at all.
This lesson unlocks