Exception Handling in Python

A program that is perfectly legal Python can still fail while it runs: a user types letters where a number was expected, a file is missing, a divisor turns out to be zero. Python reports each of these as an exception — an object describing what went wrong — and, unless the program deals with it, stops. This chapter separates the errors Python catches before running anything from the ones that only appear at run time, names the built-in exceptions the paper expects you to recognise, and then follows the flow of control through try, except, else and finally line by line. That flow is what the code-output questions test, so trace it rather than guess.

Syntax Errors and Exceptions

Syntax errors, also called parsing errors, are detected when Python reads the code, before a single statement runs: a missing colon after if, an unclosed bracket, a misspelt keyword. Python reports the line and the kind of error, and nothing runs until it is fixed. An exception is different: the code is syntactically correct, but something goes wrong during execution — dividing by zero, opening a file that does not exist, converting "abc" to an integer. When that happens Python creates an exception object and, if nothing handles it, the program terminates abruptly with a traceback. Syntax errors are themselves reported as an exception class, SyntaxError, but they cannot be handled by the program that contains them, because that program never starts.

Built-in exceptions the book lists, and what raises each
ExceptionRaised whenExample
SyntaxErrorthe code breaks Python's grammarif x > 2 (no colon)
ValueErroran argument has the right type but an inappropriate valueint("abc"), int("3.5")
TypeErroran operation is applied to a value of the wrong type"5" + 5
ZeroDivisionErrorthe divisor of a division or modulo is zero10 / 0, 7 % 0
IndexErrora sequence index is out of range[10, 20, 30][3]
NameErrora variable name is not definedprint(totl) when only total exists
IndentationErrorthe indentation is incorrecta loop body not indented
IOErrora file cannot be opened (in Python 3 an alias of OSError; a missing file gives FileNotFoundError)open("nofile.txt")
ImportErrorthe requested module or name cannot be foundfrom math import sqroot
EOFErrorthe end of input or of a file is reached while reading datapickle.load() past the last object
KeyboardInterruptthe user presses the interrupt key (Ctrl+C) during executionstopping a long loop
OverflowErrora calculation exceeds the limit of a numeric typemath.exp(1000)
⚠️ ValueError or TypeError?
int("3.5") is a ValueError: int() accepts a string, so the type is fine, but "3.5" is not a valid integer literal. "5" + 5 is a TypeError: + cannot join a str and an int whatever their values. Ask "is the type itself wrong, or only this value?" and the pair stops being confusing.

Raising Exceptions: raise and assert

Python raises exceptions on its own, and a program can raise one deliberately. The raise statement takes the form raise ExceptionName(optional message) — for example raise ValueError("marks cannot be negative"). As soon as raise executes, the rest of the current block is skipped and Python looks for a handler. The assert statement tests a condition the programmer expects to be true: assert expression, message. If the expression is True nothing happens; if it is False, Python raises AssertionError carrying the message. assert is a quick check on input or on an assumption, such as a number being non-negative before its square root is taken.

For example, this fragment prints 13 and then 4 — the line after raise never runs:
try:
print("1", end="")
raise IndexError
print("2", end="")
except IndexError:
print("3", end="")
print("4")

Handling Exceptions: try and except

When an error occurs, Python creates an exception object and hands it to the runtime system — this is called throwing the exception. The runtime system then searches backwards through the calls that led to the error for code that can deal with it, the exception handler; finding one is called catching the exception. If no handler is found anywhere, the program stops. Code that might fail is placed in a try block, and the handler in an except block that names the exception it handles. If the try block runs without an error, every except block is skipped.

  • Several except clauses may follow one try, one per exception; Python runs the first whose name matches and skips the rest.
  • An except with no exception name catches every exception. It must be the last except clause — placing it earlier is itself a SyntaxError ("default 'except:' must be last").
  • An unmatched exception — one no except clause names — is not handled here; it travels outwards to the caller, and if nothing catches it the program terminates.

A worked trace. Each item is tried in turn; "x" cannot become an integer and "0" makes the division fail, so the total is 3 + 100 + 2 + 1000 and the program prints 1105:
total = 0
for item in ["4", "x", "6", "0"]:
try:
total += 12 // int(item)
except ValueError:
total += 100
except ZeroDivisionError:
total += 1000
print(total)

The else and finally Clauses

A try statement can carry two more clauses. The else clause runs only if the try block raised no exception; it is the place for code that should run after the risky part succeeds. The finally clause runs always — whether the try block succeeded, an exception was caught, or an exception was not caught at all — so it is where clean-up goes, such as closing a file. If an exception is not handled, finally still runs first and the exception is then passed on. The order on the page is fixed: try, then the except clauses, then else, then finally.

Which clauses run
What happens in tryexceptelsefinally
No exceptionskippedrunsruns
Exception matched by an exceptthe matching one runsskippedruns
Exception matched by nonenone runsskippedruns, then the exception propagates
⚠️ finally runs even after return
In a function, a return inside try or except does not skip finally. In the fragment below, f(6, 0) reaches return -1, but finally prints "done " before the value comes back, so the line printed is done -1:
def f(a, b):
try:
return a / b
except ZeroDivisionError:
return -1
finally:
print("done", end=" ")
print(f(6, 0))
🧠 Trace with three questions
For any try statement ask, in order: (1) which line of try, if any, raises — and was anything printed before it? (2) which except, if any, matches? (3) did try finish cleanly, so that else runs? finally is printed last in every case.

Key takeaways

  • Syntax errors are caught while the code is parsed, so nothing runs; exceptions arise during execution of syntactically valid code.
  • ValueError: right type, wrong value (int("3.5")); TypeError: wrong type ("5" + 5); IndexError, NameError, ZeroDivisionError, EOFError name their cause.
  • raise throws an exception deliberately and skips the rest of the block; assert raises AssertionError when its condition is False.
  • Python runs the first matching except; a bare except catches everything and must come last.
  • else runs only when try raised nothing; finally runs in every case, even after a return, and before an unhandled exception propagates.
  • Throwing creates the exception object; catching is the runtime system finding a handler for it.

Practice questions (8)

Attempt each one before opening the answer. Every explanation names the tempting wrong option as well as the right one, because that is where marks are lost.

  1. What is the output of the following code?
    try:
    x = int("12a")
    print("A", end="")
    except ValueError:
    print("B", end="")
    else:
    print("C", end="")
    finally:
    print("D", end="")

    1. ABD
    2. BD
    3. BCD
    4. ACD
    Show answer

    Answer: B — BD

    int("12a") raises ValueError on the first line of try, so print("A") never runs; the matching except prints B, else is skipped because try did not finish cleanly, and finally prints D — BD. ABD forgets that the rest of try is abandoned the moment the exception is raised, and BCD runs else, which happens only when try raises nothing.
  2. Which exception is raised by the expression int("3.5") in Python 3?

    1. TypeError
    2. SyntaxError
    3. NameError
    4. ValueError
    Show answer

    Answer: D — ValueError

    int() accepts a string argument, so the type is acceptable; what fails is the value, because "3.5" is not a valid integer literal — that is a ValueError. TypeError is the trap: it would be raised if the type itself were unusable, as in "5" + 5. The line is grammatically valid, so it is not a SyntaxError, and every name in it is defined.
  3. In a try statement, when is the else clause executed?

    1. Only when the try block completes without raising any exception
    2. Only when an exception is raised and caught by an except clause
    3. Always, after the try block and before finally
    4. Only when no except clause matches the exception raised
    Show answer

    Answer: A — Only when the try block completes without raising any exception

    else is the success branch: it runs only if the try block finished without an exception, which keeps code that depends on that success out of the try block. "Always" describes finally, not else. When an exception is caught, the except clause runs instead of else, and when none matches, neither except nor else runs — only finally, before the exception moves on.
  4. What does the following code print?
    def f(a, b):
    try:
    return a / b
    except ZeroDivisionError:
    return -1
    finally:
    print("done", end=" ")
    print(f(6, 0))

    1. -1
    2. -1 done
    3. done -1
    4. done
    Show answer

    Answer: C — done -1

    6 / 0 raises ZeroDivisionError, the except clause reaches return -1, and finally still runs before the function actually returns, printing "done " with a space. Only then does the outer print receive -1 and print it, giving done -1. Writing -1 alone assumes return skips finally, and "-1 done" gets the order backwards, as if the value were printed before the function finished.
  5. Consider: x = -3 followed by assert x >= 0, "negative". What happens when the assert statement runs?

    1. It prints "negative" and execution continues normally
    2. It raises a ValueError with the message "negative"
    3. It raises an AssertionError with the message "negative"
    4. It sets x to 0 so that the condition becomes true
    Show answer

    Answer: C — It raises an AssertionError with the message "negative"

    assert evaluates its condition; -3 >= 0 is False, so Python raises AssertionError and attaches the message after the comma. Nothing is printed on its own — if the exception is not handled, the program stops with a traceback ending in AssertionError: negative. ValueError is a different exception that assert never raises, and assert only checks a condition; it never changes a variable.
  6. Assertion (A): A Python script with a syntax error on its last line does not print anything, even from the lines above the error. Reason (R): Syntax errors are detected when the code is parsed, before any statement is executed.

    1. Both A and R are true, and R is the correct explanation of A
    2. Both A and R are true, but R is not the correct explanation of A
    3. A is true, but R is false
    4. A is false, but R is true
    Show answer

    Answer: A — Both A and R are true, and R is the correct explanation of A

    Python reads and parses the whole script before running it, so a syntax error anywhere stops the program from starting; the print statements above the faulty line are never reached. That parsing step is exactly why A happens, so R explains A. Treating A as false confuses syntax errors with exceptions, which do let the earlier lines run because they arise only during execution.
  7. A programmer writes a bare except: clause first and an except ValueError: clause after it, under the same try. What happens?

    1. ValueError is handled by the second clause, everything else by the first
    2. Both clauses run, one after the other, whenever an exception occurs
    3. The code runs, and the second clause is simply never reached
    4. Python refuses to run the code: a bare except must be the last clause
    Show answer

    Answer: D — Python refuses to run the code: a bare except must be the last clause

    A bare except catches every exception, so any clause after it could never be reached; Python treats that ordering as an error in the code itself and reports SyntaxError: default 'except:' must be last before running anything. The idea that the later clause is "simply never reached" is the tempting answer, but Python does not wait to discover that at run time, and only one except clause ever runs for an exception.
  8. In exception handling, what is meant by "catching" an exception?

    1. Creating the exception object when an error occurs
    2. The runtime system finding and executing a suitable handler for it
    3. Detecting a syntax error before the program starts running
    4. Writing the error message into a log file on the disk
    Show answer

    Answer: B — The runtime system finding and executing a suitable handler for it

    When an error occurs, creating the exception object and handing it to the runtime system is throwing; the runtime system then searches for code that can handle it, and locating and running that handler is catching. Creating the object is throwing, the opposite end of the process. Syntax errors are found during parsing and are not caught by the program at all, and nothing about catching involves a log file.