Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Python  Module 7 – Errors, exceptions and context managers

Handling exceptions with try and except

Catch the exceptions your code can handle with try and except in Python, run code only on success with else, clean up with finally, and choose what to catch.

  • Beginner
  • 20 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free: 20 minutes

What you will learn

  • Catch specific exceptions and handle them meaningfully
  • Read the exception hierarchy to decide what to catch
  • Use else and finally clauses correctly
  • Choose between checking first and handling the exception

Before you start

int("three") cannot make a number and prices["cofee"] cannot find a key, so each of these lines raises an exception: An object Python raises when a line cannot do its job, such as the ValueError of int("three"). A try statement can handle it; otherwise it ends the program with a traceback., Python’s way of reporting that a line could not do its job. Unless something catches it, the exception ends the program with a traceback: The report Python prints when an exception ends a program: the calls that led to the failing line, oldest first, and on the last line the exception's type and message.. A try statement is how you catch one. Put the code that may fail under try:, and under except with the name of an exception type, the code that should run instead when that kind of exception happens. Python then runs your exception handler: An except clause and its block: the code that runs instead when the try block raises an exception of the class the clause names, or of a subclass of that class. and carries on after the whole statement. Any other exception passes through untouched, as if the try were not there.

Below you can step through real runs and watch where Python jumps when a line fails. You will decide how narrow a handler should be, follow else and finally along every path a try statement can take, and weigh checking first against trying first.

An exception skips the rest of the try block

This function turns the text a user typed into a quantity. It prints what it is doing at each step, so you can see which lines run:

Three calls, three different paths Python · first_try.py
def parse_quantity(text):
    print(f"parse_quantity({text!r})")
    try:
        quantity = int(text)
        print("  converted")
    except ValueError:
        print("  not a whole number, so 1")
        quantity = 1
    print("  after the try statement")
    return quantity


print(parse_quantity("3"))
print(parse_quantity("three"))
print(parse_quantity(None))  # int(None) raises TypeError, which no clause names

Output (exit status 1)

parse_quantity('3')
  converted
  after the try statement
3
parse_quantity('three')
  not a whole number, so 1
  after the try statement
1
parse_quantity(None)

Printed as an error (standard error)

Traceback (most recent call last):
  File "first_try.py", line 15, in <module>
    print(parse_quantity(None))  # int(None) raises TypeError, which no clause names
          ~~~~~~~~~~~~~~^^^^^^
  File "first_try.py", line 4, in parse_quantity
    quantity = int(text)
TypeError: int() argument must be a string, a bytes-like object or a real number, not 'NoneType'

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 first_try.py

Follow the three calls:

  1. "3" converts without trouble. The whole try block runs, the except clause is skipped, and the function carries on after the statement.
  2. "three" makes int() raise ValueError on line 4. Python abandons the rest of the try block at that line, so converted is never printed. It finds the clause except ValueError:, runs it, and then carries on after the statement as if nothing had gone wrong.
  3. None makes int() raise TypeError. No clause names TypeError, so the exception leaves the function at once. after the try statement is not printed, and the traceback ends the program.

The same three texts go through one try statement in the loop below. Step through it and watch the numbered chip, which marks the line that runs next: when int() fails on line 5, the chip jumps over line 6 to the except clause.

Interactive · line by line Three texts through one try statement Step through the loop. The lit line has just run and the numbered chip marks the line that runs next; watch where it jumps when int() fails.

Program · three_texts.py

texts = ["3", "three", None]
counts = []
for text in texts:
    try:
        counts.append(int(text))
        print("converted", text)
    except ValueError:
        print("skipped", text)
    print("after the statement")
print(counts)

Variables

After line 7: texts is ['3', 'three', None]; counts is [3]; text is None.texts'3'0'three'1None2counts30textNone
  • just taken by the loop
  • changed in this step
  • passed by the loop
  • read next

Printed so far

converted 3
after the statement
skipped three
after the statement
Error TypeError: int() argument must be a string, a bytes-like object or a real number, not 'NoneType'

Every step was recorded from a run with Python 3.14.8 on macOS 26 arm64.

The drawing shows the variables when the program ends, and the program's 18 steps are listed below.

Line run 7
Next line end
Loop pass –
Every step as text
  1. Start. No line has run yet, so there are no variables. Python starts with line 1.
  2. Line 1. texts = ["3", "three", None] makes texts a list of 3 items.
  3. Line 2. counts = [] makes counts a list of 0 items.
  4. Line 3. for text in texts: takes the item at position 0 of texts: text is '3'.
  5. Line 4. try: runs. try: checks nothing by itself. It starts the block whose exceptions the except clause below may handle.
  6. Line 5. counts.append(int(text)) adds 3 to the end of counts.
  7. Line 6. print("converted", text) prints converted 3.
  8. Line 9. print("after the statement") prints after the statement.
  9. Line 3. for text in texts: takes the item at position 1 of texts: text is 'three'.
  10. Line 4. try: runs.
  11. Line 5. counts.append(int(text)) leaves counts as it was. Key point int("three") raises ValueError before append() is called, so counts is unchanged. Line 6 is skipped: the next line is the except clause.
  12. Line 7. Line 7 runs. The ValueError matches except ValueError:, so this block runs. Afterwards the loop carries on after the whole statement, at line 9.
  13. Line 8. print("skipped", text) prints skipped three.
  14. Line 9. print("after the statement") prints after the statement.
  15. Line 3. for text in texts: takes the item at position 2 of texts: text is None.
  16. Line 4. try: runs.
  17. Line 5. counts.append(int(text)) leaves counts as it was. int(None) raises TypeError, a different kind of exception.
  18. Line 7. Line 7 runs. Key point A TypeError is not a ValueError, so the clause does not match, and no other clause is left. The exception leaves the statement and ends the program: lines 9 and 10 never run.

Three rules decide what happens. Python compares the exception with the except clauses from top to bottom and runs the first one that matches, so at most one handler runs. A clause matches the exception’s own class and every class it is derived from, which the next section uses. And except ValueError as err: gives the exception object a name, err, for the handler to use: its message is str(err) and its type type(err). Python deletes that name when the clause ends, so copy the object to another variable if you need it afterwards.

Asking again until the answer fits

A program that reads input usually needs more than one handler. This one asks for a number of seats, and its input contains two mistakes before a good answer, then ends before the second question is answered:

One try statement for each thing that can go wrong Python · ask_number.py
def ask_int(prompt, low, high):
    """Ask until the answer is a whole number from low to high; None when the input runs out."""
    while True:
        try:
            text = input(prompt)
        except EOFError:
            print("\nNo more input, giving up.")
            return None
        try:
            number = int(text)
        except ValueError:
            print(f"{text!r} is not a whole number.")
            continue
        if low <= number <= high:
            return number
        print(f"{number} is not from {low} to {high}.")


seats = ask_int("Seats (1-6): ", 1, 6)
print("Seats:", seats)
children = ask_int("Children (0-4): ", 0, 4)
print("Children:", children)

Input (standard input)

two
 9
3

Output

Seats (1-6): 'two' is not a whole number.
Seats (1-6): 9 is not from 1 to 6.
Seats (1-6): Seats: 3
Children (0-4): 
No more input, giving up.
Children: None

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 ask_number.py

Each try statement guards one call and names the one exception that call raises. input() raises EOFError when there is nothing left to read, and the function then gives up and returns None. int() raises ValueError for text that is not a whole number, and continue starts the loop again. The range check sits outside both statements, because comparing two numbers raises nothing: a number that is too big is an ordinary if, not an exception. The prompts and answers run together because the answers came from the input shown above the output rather than from a keyboard.

Key idea

When a line of the try block raises, the rest of the block is skipped and the first clause that matches the exception runs; an exception that no clause matches travels on as if the try statement were not there.

Checkpoint

Check: How a try statement handles an exception

  1. Question 1 of 2 In three_texts.py, int("three") fails on line 5. Which line runs next?

    Choose one answer.

    A hint

    Follow the numbered chip in the figure on the second pass.

    Show the answer: Question 1 of 2

    Answer: Line 7, the except clause

    When a line of the try block raises, the rest of the block is skipped. Python compares the exception with the except clause on line 7, runs its block on line 8, and then carries on after the statement, at line 9.

  2. Question 2 of 2 What does three_texts.py do with its third text, None?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: int(None) raises TypeError, which no clause names, so the program ends

    int(None) raises TypeError. The except clause names ValueError, so it does not match, and the exception travels on as if the try statement were not there. Nothing else handles it, so the traceback ends the program.

A clause catches its class and every subclass

Exception classes form a family tree, and a clause that names a class also catches every subclass: A class derived from another one, its base class, and counted as a kind of it. KeyError is a subclass of LookupError, so a clause that names LookupError also handles a KeyError. of it. The part of the tree you meet most is drawn below. Each box holds the classes derived from it, so a clause that names a box handles everything inside it:

Nested boxes of exception classes: Exception inside BaseException, and LookupError, OSError and others inside Exception.BaseExceptionSystemExit, KeyboardInterruptand GeneratorExit: not errorsException: what you catchArithmeticErrorLookupErrorOSErrorValueErrorTypeErrorNameError,AttributeError …ZeroDivisionErrorOverflowErrorIndexErrorKeyErrorFileNotFoundErrorPermissionErrorTimeoutErrorUnicodeDecodeError

Part of the exception hierarchy: each box holds the classes derived from it

Text description of the diagram

The diagram is a set of nested boxes. Each box is an exception class and holds the classes derived from it, so a clause that names a box handles every class inside it.

  • The outer box is BaseException, the root of every exception.
  • Inside it, one box names SystemExit, raised by sys.exit(), KeyboardInterrupt, raised when the user presses Ctrl-C, and GeneratorExit. They are not errors, so they stand outside Exception.
  • The other box inside BaseException is Exception, the base of everything you normally catch.
  • Inside Exception: ArithmeticError, which holds ZeroDivisionError and OverflowError; LookupError, which holds IndexError and KeyError; OSError, which holds FileNotFoundError, PermissionError and TimeoutError; ValueError, which holds UnicodeDecodeError; TypeError; and a box for NameError, AttributeError and many more.

BaseException is the root of every exception. SystemExit, raised by sys.exit(), KeyboardInterrupt, raised when the user presses Ctrl-C, and GeneratorExit sit directly under it: they are not errors, so they stand outside Exception, the base of everything you normally catch. So except LookupError: handles a bad list index and a missing dictionary key alike, and except OSError: handles every error that the operating system reports. When you handle a subclass and its base class differently, put the subclass first: Python takes the first clause that matches, so an except FileNotFoundError: written after except OSError: would never run. The full tree is on the Built-in Exceptions page of the Python documentation.

Comparing messages instead of types

if str(err) == "...": breaks when a message is reworded, and messages do change between versions: Python 3.14 rewrote the one for a list used as a dictionary key. Name the type in the clause instead, as in except KeyError:, and let the class decide.

Several exceptions in one clause

When two exceptions need the same handling, list them in one clause. Since Python 3.14 the brackets around the list are optional, as long as the clause has no as:

Two exceptions, one handler Python · bracketless.py
def to_number(value):
    try:
        return float(value)
    except ValueError, TypeError:  # Python 3.14: no brackets needed without "as"
        return None


print(to_number("2.5"), to_number("two"), to_number(None))


def describe(value):
    try:
        return f"{float(value):.2f}"
    except (ValueError, TypeError) as err:  # with "as", the brackets stay
        return f"{type(err).__name__}: {err}"


print(describe("2.5"))
print(describe("two"))
print(describe([2.5]))

Output

2.5 None None
2.50
ValueError: could not convert string to float: 'two'
TypeError: float() argument must be a string or a real number, not 'list'

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 bracketless.py

float("two") raises ValueError, while float(None) and float([2.5]) raise TypeError, and each clause handles both. With as, the brackets are still required, because without them it would be unclear what the name after as stands for:

Brackets are required with as Python · bracketless_as.py
try:
    price = float("12,50")
except ValueError, TypeError as err:
    print("Not a price:", err)

Output (exit status 1)

Printed as an error (standard error)

  File "bracketless_as.py", line 3
    except ValueError, TypeError as err:
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
SyntaxError: multiple exception types must be parenthesized when using 'as'

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 bracketless_as.py

Version note

New in Python 3.14 (PEP 758): except ValueError, TypeError: without brackets. Python 3.13 and older reject it with SyntaxError: multiple exception types must be parenthesized, so code that must run on older versions keeps the brackets: except (ValueError, TypeError): works everywhere.

Checkpoint

Check: Which exceptions a clause matches

  1. Question 1 of 2 What does this print?

    Read the code, then choose one answer.

    try:
        raise PermissionError("cannot write report.csv")
    except FileNotFoundError:
        print("missing")
    except OSError:
        print("operating system error")
    except Exception:
        print("something else")
    Show the answer: Question 1 of 2

    Answer: operating system error

    Python tries the clauses from the top. FileNotFoundError does not match a PermissionError; OSError does, because PermissionError is derived from it. Once a clause has matched, the ones below are not compared.

  2. Question 2 of 2 Which clause handles both ValueError and TypeError and names the exception err, on every Python 3 version?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: except (ValueError, TypeError) as err:

    A clause can name a tuple of exception types. Python 3.14 lets you leave out the brackets only when there is no as; with as, or for older versions, write except (ValueError, TypeError) as err:.

A broad handler hides the bugs you meant to see

A handler should name the exceptions that it knows how to deal with, and nothing else. A wider net catches mistakes you meant to see. This function was meant to return 0.0 for an empty list, but a typo in it raises a different exception:

A broad handler hides a typo Python · too_broad.py
def average_broad(marks):
    try:
        return sum(marks) / len(mark)  # a typo: mark instead of marks
    except Exception:
        return 0.0  # meant for an empty list, but it hides the typo as well


def average_narrow(marks):
    try:
        return sum(marks) / len(mark)  # the same typo
    except ZeroDivisionError:
        return 0.0  # only an empty list ends up here


print("broad:", average_broad([70, 80, 90]))
print("narrow:", average_narrow([70, 80, 90]))

Output (exit status 1)

broad: 0.0

Printed as an error (standard error)

Traceback (most recent call last):
  File "too_broad.py", line 16, in <module>
    print("narrow:", average_narrow([70, 80, 90]))
                     ~~~~~~~~~~~~~~^^^^^^^^^^^^^^
  File "too_broad.py", line 10, in average_narrow
    return sum(marks) / len(mark)  # the same typo
                            ^^^^
NameError: name 'mark' is not defined. Did you mean: 'marks'?

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 too_broad.py

except Exception: caught the NameError of the misspelt mark, so average_broad() returned a believable but wrong 0.0 instead of 80.0, and nothing pointed at the bug. average_narrow() names the only exception an empty list can cause, ZeroDivisionError, so the typo reaches the traceback, where Python even suggests the fix.

Handling an exception where nothing can be done about it

A helper that catches an exception, prints a message and returns a made-up value leaves its caller working with bad data, as average_broad() did. Let the exception travel up to the code that can retry, ask again or report it.

A bare except catches too much

except: with no type at all matches every exception, SystemExit and KeyboardInterrupt included. Here it stops sys.exit() from ending a program:

A bare except keeps a program running after sys.exit() Python · bare_except.py
import sys


def save_and_quit():
    print("Saving...")
    sys.exit(0)  # ends the program by raising SystemExit


try:
    save_and_quit()
except:  # a bare except catches SystemExit as well
    print("Something went wrong, carrying on.")
print("Still running: sys.exit() did not end the program.")

try:
    save_and_quit()
except Exception:  # SystemExit is not an Exception, so this clause lets it through
    print("Not printed.")
print("Not printed either: the program has ended.")

Output

Saving...
Something went wrong, carrying on.
Still running: sys.exit() did not end the program.
Saving...

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 bare_except.py

The first try statement swallowed the SystemExit, so the program printed that it was still running. The second one names Exception, which SystemExit is not derived from, so the exception passed through and ended the program without a traceback, as an unhandled SystemExit always does. A loop whose body sits in a try statement with a bare except: behaves the same way with Ctrl-C: the KeyboardInterrupt lands in the handler and the loop keeps going. Name the exceptions you expect, or at most Exception, which leaves both of these alone.

except: pass, or except Exception: pass

The error disappears, and so does every clue to it. If a failure really may be ignored, name its type, as in except FileNotFoundError: pass, and write a comment that says why.

Checkpoint

Check: Catching only what you can handle

  1. One question Why did the first try statement of bare_except.py keep the program running after sys.exit(0)?

    Choose one answer.

    Show the answer: One question

    Answer: sys.exit() raises SystemExit, and a bare except catches it

    sys.exit() works by raising SystemExit. A bare except: catches it like any other exception, so the program went on. except Exception: leaves SystemExit and KeyboardInterrupt alone, because neither is derived from Exception.

else holds the code that runs only on success

An else clause after the except clauses runs when the try block ran to its end without an exception; a return, break or continue that leaves the try block early skips it. It may look like a longer way to write the same thing, but it keeps the try block down to the lines whose errors you mean to handle. These two functions look up an item in a shop’s stock and then its shelf:

A second lookup inside the try block Python · else_clause.py
stock = {"pen": 12, "notebook": 4}
shelf = {"pen": "A3"}  # nobody entered the notebook's shelf


def where_is(item):
    try:
        count = stock[item]
        place = shelf[item]  # a second lookup, inside the try block
    except KeyError:
        return f"We do not sell {item!r}."
    return f"{count} on shelf {place}"


def where_is_else(item):
    try:
        count = stock[item]
    except KeyError:
        return f"We do not sell {item!r}."
    else:
        return f"{count} on shelf {shelf[item]}"  # the except clause above does not catch this


print(where_is("pen"))
print(where_is("eraser"))
print(where_is("notebook"))  # a wrong answer: notebooks are in stock
print(where_is_else("notebook"))  # the real problem, reported as a KeyError

Output (exit status 1)

12 on shelf A3
We do not sell 'eraser'.
We do not sell 'notebook'.

Printed as an error (standard error)

Traceback (most recent call last):
  File "else_clause.py", line 26, in <module>
    print(where_is_else("notebook"))  # the real problem, reported as a KeyError
          ~~~~~~~~~~~~~^^^^^^^^^^^^
  File "else_clause.py", line 20, in where_is_else
    return f"{count} on shelf {shelf[item]}"  # the except clause above does not catch this
                               ~~~~~^^^^^^
KeyError: 'notebook'

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 else_clause.py

where_is() puts both lookups in the try block, so the KeyError of the missing shelf entry is handled as if the shop did not sell notebooks: a confident, wrong answer. where_is_else() guards only the stock lookup. The shelf lookup sits in else, where a KeyError is not handled by the clause above it, so the real problem, a notebook without a shelf entry, shows up as a traceback that names it.

A try block that guards half the function

The more lines a try block holds, the more likely a handler meant for one line catches an exception from another, as where_is() did. Keep the line that may fail in the try block, and move the rest into else or after the statement.

Python Online Compiler Run else_clause.py and move the shelf lookup between the try block and the else clause to see which version reports the real problem.

Checkpoint

Check: Code that runs only on success, in else

  1. One question In else_clause.py, where_is("notebook") answers "We do not sell 'notebook'." although notebooks are in stock. Why?

    Choose one answer.

    Show the answer: One question

    Answer: The shelf lookup raised KeyError, and the clause meant for the stock lookup handled it

    The shelf has no entry for notebooks, so shelf[item] raises KeyError inside the try block, and the clause written for a missing stock entry handles it. In where_is_else() the shelf lookup sits in else, where that clause does not reach, so the traceback names the real problem.

finally runs on every way out

A finally clause runs whatever happens in the try block: when it ends normally, when it returns, and when it raises an exception that nobody here handles. That makes it the place to give back what the block borrowed, such as a connection, a lock or a temporary file:

The connection is closed on both paths Python · finally_cleanup.py
class Connection:
    def __init__(self, name):
        self.name = name
        print(f"open {name}")

    def send(self, data):
        if not data:
            raise ValueError("nothing to send")
        print(f"sent {data!r} to {self.name}")

    def close(self):
        print(f"closed {self.name}")


def upload(data):
    conn = Connection("server")
    try:
        conn.send(data)
        return "done"
    finally:
        conn.close()  # runs before the function returns, and also when send() raises


print(upload("marks.csv"))
try:
    upload("")
except ValueError as err:
    print("upload failed:", err)

Output

open server
sent 'marks.csv' to server
closed server
done
open server
closed server
upload failed: nothing to send

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 finally_cleanup.py

In the first upload, close() runs after return "done" has worked out the value and before the function hands it back. In the second, send() raises ValueError; the finally clause closes the connection, and then the exception continues to the caller, which handles it. For files and similar resources, the with statement, a later lesson of this module, does this for you, but it relies on the same rule.

One thing does not belong in a finally clause: return, break or continue. Leaving the clause that way throws away an exception that was on its way out:

return in finally discards an exception Python · finally_return.py
def risky():
    try:
        raise ValueError("the real problem")
    finally:
        return "all good"  # this discards the ValueError


print(risky())

Output

all good

Printed as an error (standard error)

finally_return.py:5: SyntaxWarning: 'return' in a 'finally' block
  return "all good"  # this discards the ValueError

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 finally_return.py

Version note

New in Python 3.14 (PEP 765): the compiler warns with SyntaxWarning when return, break or continue would leave a finally block, as above. The behaviour has not changed (the function still returns "all good" and the ValueError is lost); Python 3.13 and older do the same without a warning.

Checkpoint

Check: Clean-up in finally

  1. One question In finally_cleanup.py, upload("") raises ValueError. Which line is printed first: "closed server" or "upload failed: nothing to send"?

    Choose one answer.

    Show the answer: One question

    Answer: closed server, because finally runs before the exception leaves upload()

    send("") raises ValueError. On its way out of upload(), the finally clause runs and prints closed server; then the exception reaches the caller's except ValueError clause, which prints upload failed: nothing to send.

The four clauses run in a fixed order on three paths

A try statement needs at least one except clause or a finally clause. The clauses always come in the same order: try, then the except clauses, then else (only when there is an except clause), then finally. Which of them run depends on what the try block does:

Three paths through try, except, else and finally: no exception, an exception a clause names, and one that no clause names.NoexceptionA clausenames itNo clausenames ittry:every linerunselse:runsfinally:runscode afterthe trystatementtry:stops atthe errorthatexcept:runsfinally:runscode afterthe trystatementtry:stops atthe errorfinally:runsraised onto thecaller

What runs, and in which order, in a try statement with except, else and finally

Text description of the diagram

The diagram has three columns, one for each way a try statement can end. Each column reads from top to bottom.

  • No exception: the try block runs to its end, then the else clause runs, then the finally clause, and then the program carries on with the code after the try statement.
  • An exception that an except clause names: the try block stops at the line that raised it, the except clause that names its type (or one of its base classes) runs, then the finally clause, and then the code after the try statement.
  • An exception that no except clause names: the try block stops at the line that raised it, the finally clause runs, and the exception carries on to the code that called this one, as if there were no try statement. The else clause never runs when the try block raised an exception.

The same three texts as before now go through a statement with all four clauses. Step through it and hold each path against the diagram: "3" takes the first column, "three" the second and None the third.

Interactive · line by line Four clauses, three paths Step through the same three texts with else and finally added. Watch which clauses each text reaches before line 12.

Program · four_clauses.py

texts = ["3", "three", None]
counts = []
for text in texts:
    try:
        counts.append(int(text))
    except ValueError:
        print("skipped", text)
    else:
        print("added", text)
    finally:
        print("finally runs")
    print("after the statement")
print(counts)

Variables

After line 11: texts is ['3', 'three', None]; counts is [3]; text is None.texts'3'0'three'1None2counts30textNone
  • just taken by the loop
  • changed in this step
  • passed by the loop
  • read next

Printed so far

added 3
finally runs
after the statement
skipped three
finally runs
after the statement
finally runs
Error TypeError: int() argument must be a string, a bytes-like object or a real number, not 'NoneType'

Every step was recorded from a run with Python 3.14.8 on macOS 26 arm64.

The drawing shows the variables when the program ends, and the program's 21 steps are listed below.

Line run 11
Next line end
Loop pass 3
Every step as text
  1. Start. No line has run yet, so there are no variables. Python starts with line 1.
  2. Line 1. texts = ["3", "three", None] makes texts a list of 3 items.
  3. Line 2. counts = [] makes counts a list of 0 items.
  4. Line 3. for text in texts: takes the item at position 0 of texts: text is '3'.
  5. Line 4. try: runs.
  6. Line 5. counts.append(int(text)) adds 3 to the end of counts. No exception, so the except clause is skipped. The else block runs next, at line 9.
  7. Line 9. print("added", text) prints added 3.
  8. Line 11. print("finally runs") prints finally runs. finally runs after the else block, on the way out of the statement.
  9. Line 12. print("after the statement") prints after the statement.
  10. Line 3. for text in texts: takes the item at position 1 of texts: text is 'three'.
  11. Line 4. try: runs.
  12. Line 5. counts.append(int(text)) leaves counts as it was.
  13. Line 6. Line 6 runs. The ValueError matches, so this block runs and the else block is skipped.
  14. Line 7. print("skipped", text) prints skipped three.
  15. Line 11. print("finally runs") prints finally runs. finally runs after the except block too.
  16. Line 12. print("after the statement") prints after the statement.
  17. Line 3. for text in texts: takes the item at position 2 of texts: text is None.
  18. Line 4. try: runs.
  19. Line 5. counts.append(int(text)) leaves counts as it was.
  20. Line 6. Line 6 runs. A TypeError does not match except ValueError:. Before the exception leaves the statement, finally runs.
  21. Line 11. print("finally runs") stops the program with TypeError: int() argument must be a string, a bytes-like object or a real number, not 'NoneType'. Key point finally ran although nothing handled the TypeError. Once it ends, the exception carries on and stops the program, so lines 12 and 13 never run.

finally runs is printed three times, once for each text, but after the statement only twice. For None, the TypeError left the statement as soon as finally had run, and the program ended before lines 12 and 13.

Key idea

else runs only when the try block raised nothing, an except clause only for the exceptions it names, and finally on every way out, before an exception that nothing handled goes on.

Checkpoint

Check: The four clauses on three paths

  1. One question In four_clauses.py, int(None) fails on line 5 for the third text. Which lines run after that?

    Choose one answer.

    A hint

    Line 6 is the except clause, line 9 the else block and line 11 the finally block.

    Show the answer: One question

    Answer: Line 6, then line 11, then the program stops

    An exception that no clause names still goes through finally on its way out: line 6 is checked and does not match, line 11 prints finally runs, and then the TypeError stops the program before line 12.

Look before you leap, or ask forgiveness?

There are two ways to deal with an operation that may fail. Look before you leap (LBYL: Look before you leap: checking with an if that an operation can work before doing it. Other code can change the data between the check and the action, so the check can be out of date.) checks first, with an if. Easier to ask for forgiveness than permission (EAFP: Easier to ask for forgiveness than permission: trying an operation and handling the exception if it fails, instead of checking first. The Python documentation calls it the common Python style.) just tries, and handles the exception. Python code tends towards EAFP, and not only out of habit: between a check and the action, something else can change the data. This example books a seat on a bus both ways, with a function that stands in for another booking made at the worst moment:

A seat that disappears between the check and the booking Python · check_then_act.py
free_seats = {"12A": "window", "12B": "aisle"}


def book_lbyl(seat, meanwhile):
    if seat in free_seats:  # look before you leap
        meanwhile()  # stands in for another booking that runs at this very moment
        kind = free_seats.pop(seat)  # KeyError when the seat has gone in between
        return f"booked {seat} ({kind})"
    return f"{seat} is taken"


def book_eafp(seat, meanwhile):
    meanwhile()
    try:
        kind = free_seats.pop(seat)  # easier to ask forgiveness than permission
    except KeyError:
        return f"{seat} is taken"
    return f"booked {seat} ({kind})"


def nothing():
    pass


def someone_takes_12b():
    free_seats.pop("12B", None)


print(book_eafp("12A", nothing))
print(book_eafp("12B", someone_takes_12b))
free_seats["12B"] = "aisle"  # the seat is free again
print(book_lbyl("12B", someone_takes_12b))

Output (exit status 1)

booked 12A (window)
12B is taken

Printed as an error (standard error)

Traceback (most recent call last):
  File "check_then_act.py", line 32, in <module>
    print(book_lbyl("12B", someone_takes_12b))
          ~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "check_then_act.py", line 7, in book_lbyl
    kind = free_seats.pop(seat)  # KeyError when the seat has gone in between
KeyError: '12B'

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 check_then_act.py

book_lbyl() checked that 12B was free, but by the time it called pop() another booking had taken the seat, and the check did not protect it from the KeyError. book_eafp() has no gap to fall into: taking the seat and finding out that it has gone are one step. Step through both bookings and watch the other booking slip into the gap:

Interactive · message by message Seat 12B, booked two ways Step through a booking that checks first and one that simply tries, each with another booking taking 12B at the worst moment.
Seat 12B, booked two ways, step 7 of 7. Messages so far: Your booking to free_seats: "12B" in free_seats; Another booking to free_seats: pop("12B"); Your booking to free_seats: pop("12B"); Another booking to free_seats: pop("12B"); Your booking to free_seats: try: pop("12B").Your bookingbook_eafp("12B")free_seatsempty: {}Another bookingruns in between"12B" in free_seatspop("12B")pop("12B")pop("12B")try: pop("12B")

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. Look before you leap. The first call of the program has booked 12A, so free_seats holds 12B alone. book_lbyl() checks first: is "12B" in free_seats? It is, so the check passes and the function goes on to take the seat.
  2. Someone else is quicker. Between the check and the booking, another booking takes 12B. In the program, the function passed as meanwhile stands in for it and removes 12B from free_seats.
  3. Acting on an old answer. book_lbyl() now calls free_seats.pop("12B"), trusting a check that is no longer true. The seat has gone, so pop() raises KeyError, which nothing in the function handles.
  4. The same race, once more. book_eafp() meets the same race, with 12B free and the other booking about to take it, and it checks nothing beforehand. (In the program this booking runs first; line 31 then makes 12B free again for book_lbyl().)
  5. Someone else is quicker. The other booking removes 12B, exactly as before.
  6. Ask forgiveness. book_eafp() calls pop("12B") inside a try statement. The KeyError comes back, and its except KeyError clause turns it into the answer "12B is taken".
  7. No gap to fall into. Taking the seat and finding out that it has gone are one step, so no other booking can slip in between. The check of book_lbyl() was out of date the moment it was made.

With threads, several programs or files on disk, that gap is real, and a file can disappear between os.path.exists() and open() in the same way.

LBYL is still the clearer choice when nothing else can change the value in between, and the test is cheap and says what you mean: if divisor == 0: reads better than catching ZeroDivisionError from a long expression. It is also the safer choice when a failure halfway would leave work half done, such as checking that every field of a form is filled in before you save any of them.

Checkpoint

Check: Checking first or trying first

  1. One question When is checking first with an if the clearer choice?

    Choose one answer.

    Show the answer: One question

    Answer: When nothing else can change the value in between and the test says what you mean

    LBYL is clear and safe when the value cannot change between the check and its use, and the test is cheap. When threads, other programs or files on disk can change the data in between, trying and handling the exception has no gap.

Practise: divide safely and collect bad values

The exercise asks for two functions that each handle exactly the exception they expect and let every other one through. safe_divide() turns a division by zero into None, and parse_ints() collects a message for each text that is not a whole number, while the TypeError of a bug and the KeyboardInterrupt of Ctrl-C still get out. Its sample tests run in your browser:

Exercise · Easy · Python

Divide safely and collect bad values

Write two functions in safe_parse.py. Each one handles exactly the exception it expects and lets every other exception through.

  • safe_divide(a, b) returns a / b, or None when b is 0. A text such as "7" is a mistake in the code that called it, so safe_divide("7", 2) must still raise TypeError.
  • parse_ints(strings) converts each text of a list to an int and returns a pair (values, errors): values is the list of the numbers it could convert, in order, and errors has one message for each text that is not a whole number, written as f"{text!r} is not a whole number". parse_ints(["10", "ten", "7"]) returns ([10, 7], ["'ten' is not a whole number"]). Spaces around a number are fine, as int() allows them.

A value that is not text at all, such as None, is a bug in the caller: parse_ints(["1", None]) raises TypeError instead of collecting it. And when the user presses Ctrl-C while the list is converted, KeyboardInterrupt must stop the function, not end up in errors. One sample test fakes that moment with a text whose conversion raises KeyboardInterrupt.

Starter code · safe_parse.py

def safe_divide(a, b):
    """Return a / b, or None when b is 0."""
    # Replace this line with your code.
    return a / b


def parse_ints(strings):
    """Return (values, errors): the whole numbers of strings, and a message for each text that is not one."""
    # Replace this line with your code.
    return [], []
The sample tests · test_safe_parse.py
from safe_parse import parse_ints, safe_divide


class Interrupted(str):
    """A text whose conversion is interrupted, as when the user presses Ctrl-C at that moment."""

    def __int__(self):
        raise KeyboardInterrupt

    def __float__(self):
        raise KeyboardInterrupt

    def strip(self, chars=None):
        raise KeyboardInterrupt


def test_divide():
    """divides as / does"""
    assert safe_divide(7, 2) == 3.5
    assert safe_divide(-9, 3) == -3.0


def test_divide_by_zero():
    """returns None when b is 0"""
    assert safe_divide(1, 0) is None
    assert safe_divide(0, 0) is None


def test_divide_lets_type_error_through():
    """raises TypeError for a text: that is a bug in the caller, not a zero"""
    try:
        safe_divide("7", 2)
    except TypeError:
        return
    assert False, "safe_divide('7', 2) should raise TypeError"


def test_parse_good_values():
    """converts every whole number, with spaces around it allowed"""
    assert parse_ints(["4", " 15 ", "-2"]) == ([4, 15, -2], [])
    assert parse_ints([]) == ([], [])


def test_parse_collects_bad_values():
    """keeps going and describes each text that is not a whole number"""
    values, errors = parse_ints(["10", "ten", "", "3.5", "7"])
    assert values == [10, 7]
    assert errors == ["'ten' is not a whole number", "'' is not a whole number", "'3.5' is not a whole number"]


def test_parse_lets_type_error_through():
    """raises TypeError for a value that is not text"""
    try:
        parse_ints(["1", None])
    except TypeError:
        return
    assert False, "parse_ints(['1', None]) should raise TypeError"


def test_ctrl_c_is_not_swallowed():
    """lets KeyboardInterrupt through instead of treating it as a bad value"""
    try:
        parse_ints(["1", Interrupted("2"), "3"])
    except KeyboardInterrupt:
        return
    assert False, "parse_ints caught KeyboardInterrupt"
A hint

Put only the line that can fail inside try, and name the one exception you expect: ZeroDivisionError for the division, ValueError for int(text). In the loop of parse_ints, append the number in the try block (or in an else clause) and the message in the except clause, so that the loop carries on with the next text. A bare except: or except Exception: would also catch the TypeError and the KeyboardInterrupt that the tests want to see.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

The questions below cover handlers, matching clauses, else, finally and the two styles of dealing with failure. You get one at a time, starting where your answers are weakest, and each choice says why it is right or what it misses:

Practice that adapts to you

Practise this lesson

20 questions about 7 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 20 An exception matches two except clauses of the same try statement. How many of them run?

    Choose one answer.

    Show the answer: Question 1 of 20

    Answer: Only the first one that matches

    At most one except clause runs. Python takes the first clause, from the top, whose class is the exception's own class or one of its base classes, runs it, and carries on after the statement.

  2. Question 2 of 20 What does this print?

    Read the code, then choose one answer.

    try:
        print("a")
        n = int("4x")
        print("b")
    except ValueError:
        print("c")
    print("d")
    Show the answer: Question 2 of 20

    Answer: a c d

    The try block runs until int("4x") raises ValueError. The rest of the block, print("b"), is skipped, the except clause prints c, and the line after the statement prints d.

  3. Question 3 of 20 What does this print?

    Read the code, then choose one answer.

    def parse(text):
        try:
            return int(text)
        except ValueError:
            return 0
    
    try:
        print(parse(None))
    except TypeError:
        print("caller handled it")
    Show the answer: Question 3 of 20

    Answer: caller handled it

    An exception that a try statement does not name travels on to the code that called the function, as if that try statement were not there. Here the caller names TypeError, so print(parse(None)) is abandoned and its handler runs.

  4. Question 4 of 20 Which of these exceptions does a clause written except LookupError: handle?

    Choose every answer that is right.

    Show the answer: Question 4 of 20

    Answer:

    • IndexError, from a list index past the end
    • KeyError, from a missing dictionary key

    A clause matches the class it names and every class derived from it. IndexError and KeyError are both subclasses of LookupError; ValueError is not, and ZeroDivisionError is an ArithmeticError.

  5. Question 5 of 20 Which of these clauses is a SyntaxError in Python 3.14?

    Choose one answer.

    Show the answer: Question 5 of 20

    Answer: except ValueError, TypeError as err:

    Python 3.14 (PEP 758) lets you leave out the brackets around several exception types only when the clause has no "as". With "as", the brackets are required, so the third line fails with "multiple exception types must be parenthesized when using 'as'".

  6. Question 6 of 20 no-such-file.txt does not exist. What does this print?

    Read the code, then choose one answer.

    try:
        open("no-such-file.txt")
    except OSError:
        print("OSError")
    except FileNotFoundError:
        print("missing file")
    Show the answer: Question 6 of 20

    Answer: OSError

    Python takes the first clause that matches, and except OSError: matches every subclass of OSError. To handle a missing file in its own way, put except FileNotFoundError: above except OSError:.

  7. Question 7 of 20 average_broad([70, 80, 90]) in too_broad.py returned 0.0 instead of 80.0. Why?

    Choose one answer.

    Show the answer: Question 7 of 20

    Answer: except Exception also caught the NameError from the misspelt mark

    len(mark) raises NameError, because the name is marks. except Exception: catches it like any other error and returns 0.0, a believable but wrong answer, and nothing points at the bug. Naming ZeroDivisionError, the only error an empty list causes, lets the typo reach the traceback.

  8. Question 8 of 20 A loop's body is wrapped in try: and a bare except: that prints "skipped". What happens when the user presses Ctrl-C while it runs?

    Choose one answer.

    Show the answer: Question 8 of 20

    Answer: The handler catches the KeyboardInterrupt, prints "skipped", and the loop carries on

    A bare except: matches every exception, KeyboardInterrupt and SystemExit included, even though they are not derived from Exception. Naming the exceptions you expect, or at most Exception, lets Ctrl-C end the program.

  9. Question 9 of 20 A function reads a port with int(settings["port"]). A missing key is a bug in the caller; text that is not a number should give port 80. Which handler fits?

    Choose one answer.

    Show the answer: Question 9 of 20

    Answer: except ValueError: return 80

    Name the one exception you can do something about. Bad text has a sensible answer, port 80; a missing key means the calling code is wrong, so it should reach a traceback where someone will see it.

  10. Question 10 of 20 When does the else clause of a try statement run?

    Choose one answer.

    Show the answer: Question 10 of 20

    Answer: When the try block ran to its end without an exception

    else runs only when the try block finished without raising and was not left early by return, break or continue. Its job is to keep the try block down to the lines whose errors you mean to handle.

  11. Question 11 of 20 settings is {"port": "8080"}. What does read_port(settings) do?

    Read the code, then choose one answer.

    def read_port(settings):
        try:
            text = settings["port"]
        except KeyError:
            return 80
        else:
            return int(settings["Port"])
    Show the answer: Question 11 of 20

    Answer: It raises KeyError, because the except clause does not handle errors raised in else

    The lookup in the try block succeeds, so else runs. Its lookup uses the wrong key, "Port", and the KeyError it raises is not handled by the except clause of the same statement, so it reaches the caller. That is the point of else: only the try block's errors go to the handlers.

  12. Question 12 of 20 What does this print?

    Read the code, then choose one answer.

    def check(n):
        try:
            if n < 0:
                return "negative"
        except TypeError:
            return "not a number"
        else:
            return "fine"
        finally:
            print("checked")
    
    
    print(check(-5))
    Show the answer: Question 12 of 20

    Answer: checked negative

    The try block returns before it reaches its end, so the else clause never runs. On the way out, finally prints checked, and only then does print() in the caller show negative.

  13. Question 13 of 20 What does finally_cleanup.py print?

    What does this program print? Choose one answer.

    class Connection:
        def __init__(self, name):
            self.name = name
            print(f"open {name}")
    
        def send(self, data):
            if not data:
                raise ValueError("nothing to send")
            print(f"sent {data!r} to {self.name}")
    
        def close(self):
            print(f"closed {self.name}")
    
    
    def upload(data):
        conn = Connection("server")
        try:
            conn.send(data)
            return "done"
        finally:
            conn.close()  # runs before the function returns, and also when send() raises
    
    
    print(upload("marks.csv"))
    try:
        upload("")
    except ValueError as err:
        print("upload failed:", err)
    Show the answer: Question 13 of 20

    Answer: it prints

    open server
    sent 'marks.csv' to server
    closed server
    done
    open server
    closed server
    upload failed: nothing to send

    The finally clause runs before the function hands back its return value, so "closed server" comes before "done". In the second upload, send() raises ValueError: finally closes the connection first, and only then does the exception reach the caller's except clause, which prints "upload failed".

  14. Question 14 of 20 A try block raises ValueError, and its finally clause runs return "all good". What happens?

    Choose one answer.

    Show the answer: Question 14 of 20

    Answer: The ValueError is discarded and the function returns "all good"

    A return, break or continue that leaves a finally block discards the exception that was on its way out, which is why finally_return.py printed all good. Since Python 3.14 (PEP 765) the compiler warns about it.

  15. Question 15 of 20 In which of these cases does a finally clause run?

    Choose every answer that is right.

    Show the answer: Question 15 of 20

    Answer:

    • The try block runs return
    • The try block raises an exception that no clause handles
    • The try block ends normally

    finally runs whatever happens in the try block: a normal end, a return, an exception that a clause handles and one that nobody handles. That is what makes it the place to give back what the block borrowed.

  16. Question 16 of 20 Put the clauses of a full try statement in the order Python requires.

    Give each item its position, from 1 (first).

    Show the answer: Question 16 of 20

    Answer:

    1. try
    2. except
    3. else
    4. finally

    The try block comes first, then one or more except clauses, then else (allowed only after an except clause), and finally last of all.

  17. Question 17 of 20 A try statement has except KeyError, else and finally clauses. The try block succeeds, and then the else block raises KeyError. What happens?

    Choose one answer.

    Show the answer: Question 17 of 20

    Answer: finally runs, then the KeyError goes on to the caller

    else runs because the try block raised nothing. Its KeyError is not one the except clauses handle, so it leaves the statement, and finally runs on the way out, as it does on every path.

  18. Question 18 of 20 In check_then_act.py, why did book_lbyl() raise KeyError although it had checked that 12B was free?

    Choose one answer.

    Show the answer: Question 18 of 20

    Answer: Another booking took the seat between the check and pop()

    Look before you leap leaves a gap between the check and the action. Another booking used it to take 12B, so pop() found no key, and the check had protected nothing.

  19. Question 19 of 20 A clean-up job may delete a report file at any moment. Which code reads it safely?

    Choose one answer.

    Show the answer: Question 19 of 20

    Answer: Open it inside try, with except FileNotFoundError for a file that has gone

    When something else can change the data between a check and its use, try the operation and handle the one exception that means "it is not there", here FileNotFoundError.

  20. Question 20 of 20 A sign-up form has five fields, and nothing may be saved unless all five are filled in. How do you check?

    Choose one answer.

    Show the answer: Question 20 of 20

    Answer: Check every field with an if before saving any of them

    Trying first suits one step that may fail. When several steps must all succeed or none should happen, check everything first; with nothing else changing the form, the checks cannot go out of date.

Interview questions

Fresher · In a try statement with except, else and finally clauses, which clauses run when the try block succeeds, and which run when it raises an exception that one except clause handles?

When the try block runs to its end without an exception, the except clauses are skipped, else runs, then finally, and the program continues after the statement. When a line of the try block raises an exception that a clause names, the rest of the try block is skipped, the first matching except clause runs, else is skipped, and finally runs before the program continues after the statement. Only one except clause runs, even if several would match. If the exception matches no clause, finally still runs, and then the exception carries on to the caller. A return in the try block also passes through finally on its way out.

Key takeaways

  • A try statement runs its block; when a line raises an exception, the rest of the block is skipped and the first except clause whose type matches runs. Unmatched exceptions continue to the caller.
  • A clause matches the class it names and every subclass, so except LookupError: handles IndexError and KeyError; put subclasses before their base classes. Python 3.14 accepts except ValueError, TypeError: without brackets, and with as the brackets stay.
  • Name the exceptions you can handle. except Exception: hides bugs such as typos, and a bare except: also stops sys.exit() and Ctrl-C from working.
  • else holds the code that should run only when the try block succeeded, so its errors are not handled by mistake; finally runs on every path and suits clean-up, but never return from it.
  • Prefer trying and handling the exception (EAFP) when the data can change between a check and its use; check first (LBYL) when the test is cheap and nothing can change in between.

Terms in this lesson

EAFP
Easier to ask for forgiveness than permission: trying an operation and handling the exception if it fails, instead of checking first. The Python documentation calls it the common Python style.
exception
An object Python raises when a line cannot do its job, such as the ValueError of int("three"). A try statement can handle it; otherwise it ends the program with a traceback.
exception handler
An except clause and its block: the code that runs instead when the try block raises an exception of the class the clause names, or of a subclass of that class.
LBYL
Look before you leap: checking with an if that an operation can work before doing it. Other code can change the data between the check and the action, so the check can be out of date.
subclass
A class derived from another one, its base class, and counted as a kind of it. KeyError is a subclass of LookupError, so a clause that names LookupError also handles a KeyError.
traceback
The report Python prints when an exception ends a program: the calls that led to the failing line, oldest first, and on the last line the exception's type and message.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.