Skip to main content

Handling exceptions: try, except and raise

A try statement catches an exception before it stops the program, and raise stops a function early with a clear message. Learn the flow of try, except, else and finally, catch only what you expect, and load a dirty data file without losing its good rows.
P01.8.2L0 Foundation25 minIn review

Before this lesson

P01.8.1 Reading a traceback P01.7.4 Tables in files: CSV and JSON

After this lesson you can

  • Trace a try statement with except, else and finally, and say which blocks run when the try block raises and when it does not.
  • Catch the narrowest exception you expect, bind it with as, and explain why a bare except hides real bugs.
  • Raise an exception with a clear message of your own when a function is given input it cannot use.
  • Write a loader that skips and counts the bad lines of a data file and still returns every good row.

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.

The question

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?

Show answer

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?

Show answer

They are listed oldest first, so the frame where the error happened is the last entry, just above the error line.

Intuition

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.

A flow chart. Start the try statement, then run the try block, then a diamond: did a line raise? False leads to run the else block. True leads, along a purple path, to skip the rest of the try block, then a second diamond: does an except name its type? True leads to run that except block; False leads along a purple path round the side. All three paths meet at run the finally block. A last diamond asks: is an exception still unhandled? False leads to go on after the statement; True leads to pass it up to the caller. A note says on every path, the finally block runs last.
P01.8.2-F1

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.

Build it up

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

try:
    score = int('n/a')
except ValueError:
    score = None
print(score)
None

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.

try:
    score = int('n/a')
except ValueError as error:
    print('could not read the score:', error)
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.

Two stacks of three frames: <module>, load_rows and parse_line. Top: no try anywhere; all three frames are dashed, a purple arrow climbs from parse_line, where int('n/a') raises ValueError, past load_rows and <module>, and the program stops and prints the traceback. Bottom: a try in load_rows; the purple arrow climbs only from parse_line to load_rows, whose frame has a green edge and a tick; except (ValueError, IndexError) matches, skipped goes up by 1 and the loop moves on; only parse_line's frame is dashed, and <module> never sees the error.
P01.8.2-F2

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.

def to_int(text):
    try:
        value = int(text)
    except ValueError:
        print('  except: not a whole number')
        value = None
    else:
        print('  else: no exception')
    finally:
        print('  finally: always runs')
    return value


print(to_int('55'))
print(to_int('n/a'))
  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.

def mean(values):
    if len(values) == 0:
        raise ValueError('mean of an empty list')
    return sum(values) / len(values)


try:
    mean([])
except ValueError as error:
    print('ValueError:', error)
print(mean([55, 65, 80, 40, 60]))
ValueError: mean of an empty list
60.0

The formal statement

NoteDefinition: the try statement
try:
    <block>
except (ErrorA, ErrorB) as error:
    <handler>
else:
    <runs if the block raised nothing>
finally:
    <runs last, in every case>

There may be several except clauses, each naming one type or a tuple of types; as error is optional. The else and finally clauses are optional too.

The rules, from the Python Tutorial (8.3, 8.7) and the Language Reference (8.4):

  1. The try block runs first, line by line.
  2. If no exception is raised, the except clauses are skipped and the else block runs.
  3. If a line raises, the rest of the try block is skipped. The except clauses are tested from the top, and the first one that names the exception’s type runs. At most one handler runs.
  4. If no except clause matches, the exception goes on to the caller, as if there were no try.
  5. The finally block runs last on every path. If an exception is still unhandled, it carries on to the caller after the finally block.

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:

raise ValueError('mean of an empty list')

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.

Worked example

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, skipped

Trace it by hand over the seven lines that follow the header.

A trace table of the seven data lines of the file. Lines 2, 3, 5, 6 and 8, for Ama, Ben, Chen, Dina and Eli, each add a row, and the count of kept rows goes 1, 2, 3, 4, 5. Line 4, Femi,3,n/a, is shaded purple: int('n/a') raises ValueError, caught, and skipped becomes 1. Line 7, Gus,4, is shaded purple: parts[2] raises IndexError, caught, and skipped becomes 2. Below: load_rows returns 5 rows and skipped = 2, and the mean of the kept scores is 60.0.
P01.8.2-F3

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.

  • Line 4, 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.
  • Line 7, 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.
  • Every other line raises nothing, so the else block appends the row.

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.

Code

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

Explore

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.

Pitfalls

Wrong, with a cross: load_rows with one try around the whole for loop, the try block lines tinted purple; it keeps 2 rows with skipped = 1, because Femi ends the loop and Chen, Dina, Gus and Eli are never read. Right, with a tick: load_rows with the try inside the loop around row = parse_line(line), with else: rows.append(row), tinted green; it keeps 5 rows with skipped = 2, because a bad line is skipped and the loop goes on.
P01.8.2-F4

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, with a cross: the loader with a typo, parse_line(lne), and a bare except: it prints 0 rows kept, skipped = 7, with no message, because every line raised NameError and the bare except hid it. Right, with a tick: the same typo with except (ValueError, IndexError); the NameError is not named, so it is not caught, and the traceback ends NameError: name 'lne' is not defined. Did you mean: 'line'?, pointing at the typo.
P01.8.2-F5

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.

Check yourself

Recall

1. When does the else block of a try statement run?

Show answer

Only when the try block raised no exception.

2. When does the finally block run?

Show answer

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?

Show answer

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?

Show answer

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?

Show answer

ValueError from int('n/a'), and IndexError from parts[2], because 'Gus,4' splits into only two parts.

Exercises

1. ★ What does to_int('7') print and return? And to_int('seven')?

Show solution

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.

Show solution
def mean_hours(rows):
    if len(rows) == 0:
        raise ValueError('no rows')
    total = 0
    for row in rows:
        total += row['hours']
    return total / len(rows)


try:
    print(mean_hours([]))
except ValueError as error:
    print(error)

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?

Show solution

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.

Summary card

Catch only what you expect, as close to the risky call as you can; raise clear errors of your own.

The anchor flow chart again: run the try block; if a line raised and an except names its type, run that except block, otherwise the else block; on every path run the finally block; an exception still unhandled goes up to the caller.
P01.8.2-F1

The try statement, path by path.

  • try / except: a matching exception is caught; the rest of the try block is skipped.
  • as error: binds the caught exception, to read its message.
  • else / finally: else runs if nothing was raised; finally runs last, always.
  • raise ValueError(‘message’): stop early and say what was wrong.

Sources and next steps

Sources. Each was opened and checked against this lesson on 12 November 2026.

  • Python Software Foundation, The Python Tutorial, chapter 8: Errors and Exceptions. Section 8.3 gives the four steps of the try statement (the try clause runs; if no exception occurs the except clause is skipped; if one occurs the rest of the clause is skipped and a matching except clause runs; otherwise the exception “is passed on to outer try statements”), and says the else clause is for code that must run “if the try clause does not raise an exception”. Section 8.4 introduces raise, and section 8.7 says the finally clause “runs whether or not the try statement produces an exception”.
  • Python Software Foundation, The Python Language Reference, 8.4: The try statement. The except clauses are inspected “in turn until one is found that matches”; an expression-less except clause “matches any exception”; an unhandled exception is re-raised at the end of the finally clause.
  • Python Software Foundation, Built-in Exceptions (a clause naming a class also handles the classes derived from it; LookupError covers IndexError and KeyError) and csv: DictReader (missing values are filled in with restval, which defaults to None; open the file with newline='').
  • Ana Bell, MIT OpenCourseWare 6.100L, Fall 2022, Lecture 13: Exceptions, Assertions. Separate except clauses for ValueError and ZeroDivisionError, else and finally, and the advice against failing silently: raise an exception instead.
  • Pablo Galindo Salgado and Brett Cannon, PEP 758, Allow except and except* expressions without parentheses. Final in Python 3.14; with 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

P01.8.3 Debugging as a method

Your progress is saved in this browser only. No account, no tracking.
Back to top