Chapter 1: Exception Handling in Python

On 4 June 1996, a new European launch vehicle, Ariane 5, rose from its pad at Kourou in French Guiana on its first flight. Thirty-seven seconds later it broke up and was destroyed. It carried no crew, only satellites, but it carried a lesson that every programmer has been taught from ever since.

The engines were fine. The failure was in software. A navigation routine inherited from the earlier and slower Ariane 4 was still running after lift-off, although it was no longer needed. That routine converted a number describing the vehicle’s sideways motion from a large numeric format into a much smaller one. On the faster Ariane 5 the value was too big for the smaller format, and the conversion could not be done.

The program had no instructions for that situation. It stopped, and passed on a diagnostic pattern that the flight computer read as if it were steering data. The nozzles swung hard over, the vehicle turned across the airflow, and it tore itself apart. The backup computer, running the very same code, had already failed in the very same way.

Nothing here was a typing mistake. The code ran. It simply had no plan for a situation its authors believed could never arise — the two possible endings sketched in Fig. 1.0.

Fig. 1.0

Fig. 1.0 – Program flow with and without a plan for errors.

NoteCuriosity Corner

Q1. A program can be free of spelling and punctuation mistakes and still stop in the middle of a run. How is that possible? Q2. When Python meets a problem while running, it prints a name for it. Where do those names come from, and how many of them does a programmer need to know? Q3. Can a programmer deliberately make a program signal an error, even when Python itself sees nothing wrong? Why would anyone want to? Q4. If only three lines of a long program are risky, must the whole program be rewritten to protect against a failure in those three lines? Q5. If a program is going to stop anyway, can it still be made to finish some last piece of work — such as closing a file it opened — before it goes?

This chapter answers all five.

1.1 Introduction

You have written enough programs by now to know that a program can disappoint you in more than one way. Sometimes it refuses to start at all. Sometimes it starts, prints two or three lines, and then stops in the middle with a block of red text. And sometimes — the worst case — it runs quietly from beginning to end, prints an answer, and the answer is wrong.

These are three different kinds of trouble, not three moods of the same one. They are caught at different moments, they are reported differently, and each is repaired in a different way. Before you can protect a program against failure, you have to know which of the three you are looking at.

Errors caught before anything runs

The first kind is a syntax error. Every programming language has rules about how a statement may be written — where a colon goes, where brackets must close, how a string is quoted, how a block is indented. Break one of those rules and Python cannot even work out what you are asking it to do.

marks = 78
if marks > 60
    print("Passed")

The colon after the condition is missing. Nothing in this fragment is impossible for the computer to carry out; it is simply not written in Python. Python examines the whole file before executing anything, finds the offending line, and refuses to begin. Notice what this means: the first line, marks = 78, is perfectly correct, but it never runs either. A syntax error is not a partial failure. The program does not start.

Errors that appear only while running

The second kind appears in code that obeys every rule of the language. Python reads it, accepts it, begins executing it — and then meets an operation it cannot actually perform on the values it has been given. This is a run-time error.

n = int(input("Enter a number: "))
print(100 / n)

Read those two lines carefully. There is nothing wrong with them. Division is a legal operation, int() is a legal function, and both statements are correctly written. Whether this program survives depends entirely on what the user types. Type 4 and it prints 25.0. Type 0 and division becomes impossible, because no number multiplied by zero gives 100. Type four and int() is handed something it cannot convert to a whole number. In either of those two cases Python stops at that statement and prints a report naming what went wrong.

The important point is that the error lives in the meeting of the code and the data, not in the code alone. The same two lines are correct for one input and fatal for another. This is why run-time errors cannot be found merely by reading a program, and why they are the errors a program has to be built to survive.

Errors nothing can catch for you

The third kind is a logical error. Here the program is legal, every operation it attempts succeeds, it runs to the last line and prints an answer — and the answer is not the answer to the problem you were solving. Python has no way of knowing this. It was asked to compute something, and it computed exactly that; only you know it was the wrong something.

the loop that stops one element early What students write:

marks = [72, 65, 80, 91]
total = 0
for i in range(len(marks) - 1):
    total = total + marks[i]
average = total / len(marks)
print("Average marks:", average)

What happens: the program runs without any complaint and prints

Average marks: 54.25

The list has four marks adding up to 308, so the average should be 77.0. Writing len(marks) - 1 makes the loop take i as 0, 1 and 2 only, so the last mark, 91, is never added — while the division still divides by 4.

The fix:

for i in range(len(marks)):

range(n) already stops one short of n, so subtracting 1 as well drops a genuine element. With the correction the program prints Average marks: 77.0.

The three kinds are worth setting side by side, because the question “when is it caught?” separates them cleanly.

Table 1.1 – syntax error vs run-time error vs logical error | Feature | Syntax error | Run-time error | Logical error | |—|—|—|—| | What is wrong | The statement is not written according to the rules of Python | The statement is legal, but the operation cannot be carried out on the values supplied | The statements are legal and all succeed, but they compute the wrong thing | | When it is detected | Before execution begins | During execution, at the offending statement | Only when a human checks the output | | How much of the program runs | None of it | Everything up to that statement | All of it | | Who or what reports it | Python, with the position of the error | Python, with a name and a description of the trouble | Nobody; the program appears to have worked | | How it is repaired | Correct the way the line is written | Anticipate the situation and write code that deals with it | Rethink the method and correct the logic |

The order in Fig. 1.1 makes the same point as a sequence: a program has to pass one gate before it can fail at the next.

Fig. 1.1

Fig. 1.1 – Execution flow and the three kinds of programming errors.

Q. A program prints its answer without a single error report. Does that prove it is correct?

A. No. It proves only that every statement was legally written and every operation it attempted could be carried out. Whether the answer is the right answer is a separate question, and the average program above answers it: it printed 54.25 without complaint.

Q1. A program can be free of spelling and punctuation mistakes and still stop in the middle of a run. How is that possible?

A. Because being written correctly and being able to run are two different things. Python checks the rules of the language before it starts, so a program free of writing mistakes will always begin. But some operations can only be attempted once real values are in hand — dividing by a number that turns out to be zero, converting text that turns out not to be a number, opening a file that turns out to be missing. When such an operation cannot be carried out, Python stops at that very statement, halfway through an otherwise faultless program.

Of the three kinds, a syntax error is repaired once and is gone, and a logical error is a matter of rethinking your method. Run-time errors are different in kind: many of them cannot be argued away, because they depend on what a user types, on whether a file exists, or on data that arrives from outside the program. A program that must keep working in the real world has to expect them and be ready.

This chapter is about exactly that readiness — how Python signals a run-time problem, what those signals are called, how a programmer can deliberately trigger one, and how a program can catch a problem, deal with it, and carry on instead of collapsing. Section 1.2 begins with the errors Python catches first, so that the boundary between the two is clear.

Type the corrected average program and run it. Then change the list to a single mark, marks = [88], and predict the printed average before you press Enter. Now change the list to an empty one, marks = [], predict what will happen, and run it. Which of the three kinds of error have you just produced?

1.2 Syntax Errors

Before Python can carry out a single instruction, it has to understand what you have written. It reads the source file, breaks each line into its parts, and checks that those parts are arranged in a way the language allows: a colon where a colon is required, brackets that close, quotation marks in pairs, a block indented consistently under its header. This reading and checking is called parsing, and the part of Python that does it is the parser.

If the parser meets an arrangement it cannot make sense of, it reports a syntax error. Because the fault is found while the code is being parsed, syntax errors are also known as parsing errors — the two names describe the same thing from two angles: what rule was broken, and when it was noticed.

NoteDefinition

A syntax error is an error caused by writing a statement that does not follow the grammatical rules of the Python language. It is detected before execution begins, and no statement of the program is executed until it is corrected.

Two consequences follow, and both are worth stating plainly.

The first is that a syntax error is never partial. Python parses the whole file before it runs any of it, so a mistake on line 40 stops line 1 from running as well. You will not see the first half of the output followed by an error; you will see no output at all.

The second is that the program does not repair itself. You must find the offending line, correct it, save the file, and run the program again. If a second syntax error is waiting further down, the same cycle repeats — Python reports the first one it meets, not all of them at once.

Q. If the first ten lines are correct, why does Python refuse to run them?

A. Because parsing and executing are two separate stages, and parsing comes first for the whole file. Until Python has understood every line, it has not begun executing any line. A file with a syntax error never reaches the executing stage at all.

What the error report looks like depends a little on how you are working — typing statements one at a time at the prompt, or running a saved file.

1.2.1 Syntax Errors in Interactive Mode

When you work interactively, at the >>> prompt of the Python shell — commonly called shell mode — each statement is parsed the moment you finish typing it. A mistake is therefore reported immediately, before you can type anything else.

>>> count = 5
>>> while count > 0
  File "<stdin>", line 1
    while count > 0
                   ^
SyntaxError: expected ':'

The assignment on the first line was accepted; count now holds 5. The second line was rejected, and Python printed a small report instead of waiting for the body of the loop. Read the report from the bottom upwards, because the last line is the most informative.

The last line always has the same two parts: the name of the error, SyntaxError, and a short description of what the parser expected but did not find. Above it, Python repeats the line it was reading and marks the position with a caret. "<stdin>" simply means that the line came from what you typed rather than from a file. The labelled parts are collected in Fig. 1.2.

Fig. 1.2

Fig. 1.2 – Anatomy of a syntax error report in the shell.

The description is Python’s attempt to be helpful, and it has grown more helpful over the years. From Python 3.10 onwards a missing colon is reported as shown above; older versions print the vaguer SyntaxError: invalid syntax for the same mistake. The name of the error and the position are the parts you can rely on across versions; the wording of the description is not.

reading the reported position as the place where you made the mistake What students write:

total = (12 + 8
print(total)

What happens: the parser reads past the end of the first line still waiting for the closing bracket, so the position it reports need not be the position where you went wrong. Recent versions of Python point back helpfully to the bracket that was never closed; older ones report a problem around the second line, where nothing is wrong at all.

The fix: close the bracket — total = (12 + 8). When a reported line looks blameless, examine the line above it, and check the brackets and quotation marks there.

1.2.2 Syntax Errors in Script Mode

Most of your work is done in script mode: the statements are typed into an editor, saved as a .py file, and run as a whole. Here the effect of a syntax error is easier to see, because you can watch a program that has real work to do produce nothing whatever.

Suppose the file greet.py contains these three lines.

name = input("Enter your name: ")
print("Welcome, " + name)
print("Have a good day)

The third line opens a string with a quotation mark and never closes it. Run the file and this is what appears.

  File "/home/student/greet.py", line 3
    print("Have a good day)
          ^
SyntaxError: unterminated string literal (detected at line 3)

The report carries the same parts as before, with the file’s name and path in place of "<stdin>". What matters most, though, is what is missing from the output. The prompt Enter your name: never appeared. The parser rejected the third line before the first line was executed, so the program never asked for anything and never greeted anybody. Correct the line, save the file, and run it again; only then does anything happen.

Different tools present the same information in different ways. Run the file from a command window and you see the plain text above. Run it from an editing environment such as the integrated development and learning environment (IDLE) that comes with Python, and the error is announced in a dialog box carrying the error name in its title and the description inside, while the editor moves the cursor to the offending place in the file, as shown in Fig. 1.3. The presentation differs; the diagnosis does not.

Fig. 1.3

Fig. 1.3 – A syntax error reported in an editor and dialog box.

As with shell mode, the exact wording varies with the version of Python: before 3.10 the same unterminated string is reported as SyntaxError: EOL while scanning string literal, where EOL stands for end of line.

Everything in this section has one thing in common — the mistake is in the writing, and it is caught before the program runs. Once a file parses cleanly, that whole category of trouble is behind you. What lies ahead is the second category from Section 1.1: statements that are written perfectly and still cannot be carried out. Those are the subject of Section 1.3.

Type the three lines of greet.py correctly and run the file, so that you see it work. Now break it in three separate ways, one at a time, running the file after each: remove the closing bracket of the second line, indent the second line by four spaces, and change input to imput. Two of the three are reported before the program runs and one is not. Which one, and why?

1.3 Exceptions

A statement can obey every rule of Python and still be impossible to carry out. 100 / n is a perfectly formed expression, but if n holds 0 there is no number it can produce. open("marks.txt") is a perfectly formed call, but if no file of that name exists there is nothing to open. int(reply) is a perfectly formed conversion, but if reply holds "four" there is no whole number to convert it to.

In each case the fault is not in the writing. It is in the situation the statement meets when it actually runs. Python calls such a situation an exception.

The word is well chosen. These are the exceptional cases — the ones outside the ordinary run of things the program was designed for. A billing program is designed to divide a total by a count; it is not designed for the day the count is zero. An exception is what happens when reality steps outside the design.

What Python does at that moment

When Python meets an operation it cannot complete, it does not simply stop. It builds an object that describes what went wrong — the kind of error, the message that explains it, and the place in the program where it happened. That object is the exception. Creating it and handing it over is described by saying that an exception has been raised.

Once an exception has been raised, the normal flow of the program is interrupted at that exact statement. Python then looks for code written to deal with this kind of error. If it finds such code, the program recovers and carries on. If it finds none, execution stops and Python prints the description for you to read.

NoteDefinition

An exception is an object that represents an error detected during the execution of a program. When such an error occurs, the exception is said to be raised, and the normal flow of the program is interrupted at that statement.

the exception mechanism Statement: When an operation cannot be completed during execution, Python creates an object describing the error, interrupts the normal flow at that statement, and transfers control to code written to deal with that kind of error; if no such code exists anywhere in the program, execution stops and the description is printed. In words: an error is turned into a value the program can be given a chance to deal with, instead of an immediate collapse. Why it holds: every kind of error has a name of its own in Python, and the object carries that name along with a message and the position at which the trouble arose — enough information for the program, or the programmer, to act on. Used for: keeping the main logic of a program separate from the code that copes with failure, and allowing a program to survive conditions that depend on data rather than on code. Watch out: the mechanism can act only on errors Python is able to detect. A program that computes an average with the wrong divisor raises nothing at all; it simply reports a wrong number.

Reading an unhandled exception

Consider a small program saved as cost.py. It is written correctly throughout, and the fourth line is the one that will not survive.

print("Bill for the day")
price = 250
quantity = 0
print("Average cost per item:", price / quantity)
print("Thank you")

Running it produces this.

Bill for the day
Traceback (most recent call last):
  File "/home/student/cost.py", line 4, in <module>
    print("Average cost per item:", price / quantity)
ZeroDivisionError: division by zero

Two things in that output deserve attention, and the second is the more surprising.

First, Bill for the day was printed. Unlike a syntax error, an exception does not prevent the program from starting. Everything before the offending statement ran normally. Everything after it — including Thank you — did not run at all, because the flow was interrupted and never resumed. The program stopped in the middle, which is exactly the behaviour the opening story of this chapter began with.

Second, the words Average cost per item: were never printed either, even though they sit in the same statement. Python works out the values to be printed before it prints anything, and the division failed while those values were being worked out. The call to print() was never made at all.

The block of text beginning Traceback is Python’s report on a raised exception, and it is called the traceback. Its parts are labelled in Fig. 1.4.

Fig. 1.4

Fig. 1.4 – Anatomy of a Python runtime error traceback.

Read a traceback from the bottom up. The last line is the diagnosis: the name of the exception, ZeroDivisionError, and the message, division by zero. Above it, Python shows where the trouble happened. Here the position is given as <module>, meaning the statement sat in the main body of the file and not inside any function.

Recent versions of Python add a line of marks under the echoed statement, pointing at the exact part of it that failed. The information is the same; only the presentation has been improved.

Q. A program prints a wrong average and no traceback appears. Which kind of error is that, and can the exception mechanism help?

A. That is a logical error, and the exception mechanism cannot help. Nothing was impossible to carry out — the division succeeded and produced a number. Exceptions are raised only when an operation cannot be completed, never when it completes with a result you did not want.

Whose fault is an exception?

It is tempting to read a traceback as a scolding, but many exceptions are nobody’s mistake. A file may be missing because a user moved it. A conversion may fail because someone typed a word where a number was expected. A division may be by zero because the day’s stock register genuinely recorded no items.

This is why exceptions are handled rather than merely avoided. A programmer designing a program is expected to think ahead about the erroneous situations that may arise during its execution and to include code that addresses them, so that the program does not terminate abnormally in front of its user. Sections 1.6 and 1.7 build exactly that code. Before then, two questions have to be settled: what these errors are called, and how a programmer can raise one deliberately.

1.3.1 Syntax Errors and Exceptions Compared

A student meeting both terms in the same week usually assumes they are opposites — that a syntax error is one thing and an exception is another. The truth is slightly more interesting, and it is worth getting right.

SyntaxError is itself one of Python’s named error types, exactly like ZeroDivisionError. Look again at the report in Section 1.2 and you will see the same shape as the last line of a traceback: a name, a colon, a description. So a syntax error is an exception. What is special about it is when it arises. It is raised by the parser, while the program is being read, rather than during execution.

Every other exception is raised only after the parser has been satisfied. In other words, all the exceptions in the rest of this chapter arise in programs that are syntactically correct. This gives the statement often quoted about the two: every syntax error is an exception, but every exception is not a syntax error.

Table 1.2 – syntax error and other exceptions | Feature | Syntax error | Other exceptions | |—|—|—| | When it is raised | While the program is being parsed, before execution | During execution, at the statement that fails | | State of the program | Not a legal Python program | A legal Python program, running normally until that point | | How much has run | Nothing at all | Everything up to the failing statement | | What it depends on | The way the code is written | The values the code meets while running | | Can the program itself deal with it | No — the program never begins running | Yes; this is what exception handling is for | | Typical repair | Correct the line, save the file, run again | Anticipate the situation and write code that responds to it |

The last row carries a practical consequence that students sometimes learn the hard way. You cannot protect a program against its own syntax errors. The code you would write to catch such an error sits inside the very file Python is refusing to run, so it never gets the chance to act. A missing colon has to be repaired by you, in the editor, before the program exists at all.

Everything from here on assumes a file that parses cleanly. The remaining question is what can still go wrong inside it — and Python has already given those situations names.

Save the five-line cost.py exactly as shown and run it, so that you see where the output stops. Now change quantity to 4 and run it again. Then set it to the string "4", written with quotation marks, and predict before running whether the program will stop, and at which line. Compare the name on the last line of that traceback with the one you saw the first time.

1.4 Built-in Exceptions

Some things go wrong so often that it would be wasteful for every programmer to describe them afresh. Dividing by zero, asking for the eleventh item of a list that holds three, using a name that was never defined, opening a file that is not there — these are not exotic accidents. They happen in every language, in every kind of program, to everybody.

Python therefore comes with a ready-made set of error types for them. These are the built-in exceptions: exceptions that are already defined in Python itself, each with a name and a standard message, so that a program can name the trouble precisely instead of merely announcing that something failed.

NoteDefinition

A built-in exception is an exception that is already defined in Python, with a fixed name and a standard message, and is raised automatically by the interpreter when the corresponding error occurs during execution.

The value of a fixed name is easy to miss. Because the name is standard, it means the same thing in every Python program ever written. ZeroDivisionError never means a missing file, and IndexError never means bad data typed by a user. When you write code to respond to failure — as you will from Section 1.6 onwards — you will name the exception you are prepared for, and that response will be applied to that failure and to no other.

The exceptions you will meet most often

Table 1.3 lists the built-in exceptions that occur most commonly in the kind of programs you write. Read the middle column as the general rule and the last column as the sort of statement that triggers it.

Table 1.3 – commonly occurring built-in exceptions, when each is raised, and a situation that raises it | Name of the exception | When it is raised | A situation that raises it | |—|—|—| | SyntaxError | When the code does not follow the grammatical rules of Python; found while parsing, before execution | The colon after an if header is left out | | ValueError | When a function or an operation is given an argument of the right type but an unsuitable value | int("twelve") — text of the right type, but not a number | | IOError | When a file operation cannot be carried out, such as opening a file that is not present | Opening marks.txt for reading when no such file exists | | KeyboardInterrupt | When the user interrupts a running program from the keyboard | Pressing Ctrl-C while a long loop is running | | ImportError | When the module named in an import statement, or a name asked for from it, cannot be found | import mathematics — no such module | | EOFError | When input() reaches the end of the input without reading any data | A program reading with input() when the supplied input has run out | | ZeroDivisionError | When the divisor in a division or a modulus operation is zero | total / count where count holds 0 | | IndexError | When the index used with a sequence is outside its valid range | fares[3] on a list of three items | | NameError | When a name used in the program has not been defined | Using total before any value has been assigned to it | | IndentationError | When the indentation of the code is not correct | A statement indented under a header that does not expect a block | | TypeError | When an operation or a function is applied to a value of an unsuitable type | "Total: " + 250 — text added to a number | | OverflowError | When the result of an arithmetic operation is too large to be represented | math.exp(1000) — a floating-point result beyond the largest that can be stored |

Three entries in that table repay a closer look, because Python 3 treats them a little differently from the way their names suggest.

IOError is retained in Python 3 as another name for the more general OSError, which covers failures reported by the operating system. When the particular trouble is a missing file, Python reports it by the more precise name FileNotFoundError, which is a kind of OSError. Since the general name covers the specific one, code written to respond to IOError still responds when a file is not found.

IndentationError is not a separate species either. It is a kind of SyntaxError, which is why a badly indented file is rejected before it runs, exactly like a missing colon.

OverflowError is the entry most often misread. Whole numbers in Python are not limited to a fixed size — an integer simply grows as large as the memory allows, so 2 ** 10000 is computed without complaint. The overflow you can actually meet comes from floating-point arithmetic, where the range is fixed.

>>> import math
>>> math.exp(1000)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
OverflowError: math range error
Seeing them raised

The quickest way to become familiar with these names is to provoke them at the >>> prompt and read what comes back. Each of the sessions below is a single statement and its report.

>>> print(total + 40)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'total' is not defined
>>> print("Total: " + 250)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can only concatenate str (not "int") to str
>>> fares = [12, 15, 20]
>>> print(fares[3])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: list index out of range

The third session is worth pausing on. The list fares was created successfully on the first line; the exception came only on the second. Positions in a list are counted from 0, so a list of three fares has positions 0, 1 and 2. Asking for position 3 asks for a fare that is not there, and Python will not silently return nothing in its place.

Notice also how much the message tells you. It does not merely say that something is wrong with the list — it says the index is out of range. A traceback is not a rebuke; it is a diagnosis, and reading it carefully usually saves you from guessing.

Two names that are easily swapped

ValueError and TypeError are the pair students confuse most, because both arise when a function is unhappy about what it was given. The distinction is precise, and it is worth fixing now.

>>> int("twelve")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 10: 'twelve'

Here int() was given a string, and a string is exactly what int() expects to convert. The type was right. What was wrong was the particular value: "twelve" spells a number in English, not in digits. Give int() a list instead — a type it cannot convert at all — and the complaint changes.

Table 1.4 – ValueError and TypeError | Feature | ValueError | TypeError | |—|—|—| | What is wrong | The value supplied is unsuitable | The type of the value supplied is unsuitable | | Was the type acceptable | Yes, the argument is of a type the operation accepts | No, the operation cannot work with that type at all | | Typical cause | Data that came from a user or a file and does not have the expected form | Mixing kinds of data, or forgetting a conversion | | Example that raises it | int("twelve") | "Total: " + 250 | | Usual repair | Check the value before using it, or handle the failure | Convert the value, or correct the expression |

Q. Which of the two is raised by int("12.5")?

A. ValueError. The argument is a string, which is a type int() accepts, so the type is not the problem. The value is: "12.5" is not the way a whole number is written, and int() converts only whole numbers from text. Converting it in two steps, with int(float("12.5")), succeeds and gives 12.

Exceptions a programmer defines

The built-in set covers the errors the language itself can recognise, but it cannot cover the errors that belong to your problem. Python has no idea that a bus fare should never be negative, that an admission number must have six digits, or that a stock quantity below zero means the register was filled in wrongly. Nothing in those statements is impossible to compute; they are simply wrong for the situation.

For such cases a programmer may create an error type of their own. These are called user-defined exceptions, and they are raised deliberately by the program rather than automatically by the interpreter. The tools for raising them, and for raising built-in exceptions on purpose, are the subject of Section 1.5.

Fig. 1.5

Fig. 1.5 – Hierarchy of common built-in Python exceptions.

Fig. 1.5 shows why the names are not a flat list. ZeroDivisionError is a special kind of arithmetic error, IndexError a special kind of lookup failure, FileNotFoundError a special kind of operating-system failure. This arrangement will matter in Section 1.6, where naming a general error type turns out to cover all the particular ones beneath it.

Q2. When Python meets a problem while running, it prints a name for it. Where do those names come from, and how many of them does a programmer need to know?

A. The names are built into Python itself. Each common kind of failure has its own ready-made error type with a fixed name and a standard message, so ZeroDivisionError means the same thing in your program as in anybody else’s. There are many of them, but you do not have to memorise a catalogue: the dozen in Table 1.3 cover almost everything a Class XII program meets, and the rest announce themselves clearly on the last line of a traceback when you meet them. A programmer may also define error types of their own for conditions that belong to the problem rather than to the language.

At the >>> prompt, write one statement of your own for each of these four exceptions, so that Python raises it: NameError, IndexError, TypeError and ZeroDivisionError. Keep each statement to a single line. Then write one statement that you expect to raise ValueError but not TypeError, and check the last line of the traceback to confirm which one you actually got.

1.5 Raising Exceptions

Every exception you have seen so far arrived by itself. Python attempted an operation, found it impossible, created the exception object and interrupted the program — all without being asked. That is the interpreter doing what it can: it knows what Python cannot do.

But the interpreter does not know what your problem forbids. A mark of 130 out of 100 is an ordinary integer. A stock quantity of −5 is an ordinary integer. A requested loan of 15 books from a shelf holding 12 is arithmetic that works perfectly well. Python will compute with all three without a murmur, because nothing in the language has been broken. Only you know that these values are impossible in the situation you are modelling.

So Python allows the programmer to raise an exception deliberately, at a moment of their own choosing, using the raise and assert statements. Raising an exception means creating the exception object and interrupting the normal flow of the program so that the situation can be dealt with.

NoteDefinition

Raising an exception means signalling an error condition, so that the normal flow of execution is interrupted at that point and control passes to the code written to deal with that error. An exception may be raised automatically by the interpreter, or deliberately by the programmer.

The interruption is the whole point, and it is worth being exact about it. Once an exception is raised, no further statement in the current block of code is executed. Control leaves that block at once and goes looking for code written to handle the exception. If such code exists — Section 1.6 shows how to write it — the program carries on from there. If it does not, the program stops and prints a traceback. Fig. 1.6 shows both endings.

Fig. 1.6

Fig. 1.6 – Execution flow when an exception is raised.

1.5.1 The raise Statement

The raise statement names the exception you want raised, and optionally supplies a message to go with it.

the raise statement

raise exception_name(optional_argument)

Parts: raise is the keyword; exception_name is the name of the exception type to be raised, usually one of the built-in exceptions listed in Table 1.4; optional_argument is generally a string describing the particular situation, and may be omitted along with the brackets. Returns / Effect: an exception object of the named type is created and raised at that point. No value comes back, and no further statement in that block runs. Example:

>>> raise ValueError("Mark must lie between 0 and 100")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: Mark must lie between 0 and 100

Compare that traceback with the ones in Section 1.4 and you will see that it has exactly the same shape as an exception raised by the interpreter itself. The last line carries the name you asked for, then the message you supplied. A raised exception is not a lesser kind of exception; it is the same mechanism, started by hand.

The message is optional. Leaving it out gives a bare name and nothing after the colon — indeed, no colon at all.

>>> raise IndexError
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError

That is legal, but rarely a kindness. The name alone tells the reader which category of trouble occurred; the message tells them which particular case, and in whose terms. ValueError: Mark must lie between 0 and 100 explains itself to a person who has never read your code.

refusing an impossible book issue Problem: a library counter program reads the number of copies of a title currently in stock and the number of copies a class teacher has requested. Copies may be issued only if enough are in stock. If more are requested than exist, the program must stop at that point with a clear report, and must not go on to alter the stock figure.

stock = int(input("Books in stock: "))
asked = int(input("Books requested: "))
if asked > stock:
    raise ValueError("Requested more books than are in stock")
stock = stock - asked
print("Issued", asked, "books. Remaining stock:", stock)

Sample output:

Books in stock: 12
Books requested: 15
Traceback (most recent call last):
  File "issue.py", line 4, in <module>
    raise ValueError("Requested more books than are in stock")
ValueError: Requested more books than are in stock

Identifier description:

[Table:] Table 1.5 – identifiers used in the book issue program | Identifier | Type | Purpose | |—|—|—| | stock | int | Number of copies on the shelf; reduced once an issue is allowed | | asked | int | Number of copies requested by the teacher | [/Table]

Notes: ValueError is the right choice here because the values are of the correct type and are perfectly good integers — they are simply unsuitable for the situation. Note what the raise protects: the subtraction on line 5 never happens, so the stock figure is not corrupted by an issue that was never valid. Run the same program with 12 in stock and 5 requested and it prints Issued 5 books. Remaining stock: 7, because the condition is false and the raise is skipped.

Why raise an exception when the program stops anyway?

At this stage raise may look like an elaborate way of crashing. Two answers, and the second is the important one.

The first is that a program which stops at the fault is far easier to repair than one that carries on. Without the check, the program above would happily record an issue of 15 copies from a shelf of 12 and leave the stock at −3. That wrong number would then be written to a file, printed on a report, and discovered a week later by somebody who has no idea where it came from. Failing early and loudly is a service to whoever reads the program next.

The second is that raising is not the same as stopping. An exception is a signal, and a signal needs a listener. Section 1.6 shows how the code that calls this program’s logic can catch that ValueError, print a polite message to the person at the counter and ask them to enter the request again. The raise states the rule; it does not decide the consequence.

raising a message instead of an exception What students write:

raise "Requested more books than are in stock"

What happens: Python refuses the statement itself.

TypeError: exceptions must derive from BaseException

The fix: name an exception type and put the message inside it.

raise ValueError("Requested more books than are in stock")

A message is a string, and a string is not an exception. What is raised must be an exception object, which is why the type name comes first and the message goes inside its brackets.

1.5.2 The assert Statement

Some conditions are not really rules about the user’s data at all; they are assumptions the programmer is making. A function that computes an average assumes the list handed to it is not empty. A function that converts a temperature assumes the reading is a physically possible one. If such an assumption is false, the fault lies in the program, not in the world, and the sooner it is discovered the better.

The assert statement exists for exactly this. It tests an expression that ought to be true, and raises AssertionError if it turns out to be false.

the assert statement

assert expression, message

Parts: assert is the keyword; expression is the condition being claimed to be true; message is an optional string, written after a comma, that becomes the description attached to the exception. Returns / Effect: if the expression is true, nothing happens at all and execution continues with the next statement. If it is false, AssertionError is raised and the message, if given, appears after the colon in the traceback. Example:

>>> quantity = -5
>>> assert quantity >= 0, "Quantity cannot be negative"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError: Quantity cannot be negative

An assertion placed at the top of a function is a compact way of stating what that function requires of whoever calls it — and of making sure the requirement is actually met rather than merely hoped for.

guarding a temperature conversion with assert Problem: write a function that converts a temperature given in degrees Celsius to degrees Fahrenheit. No reading below absolute zero, −273.15 °C, is physically possible, so the function must reject such a value instead of returning a meaningless number. Demonstrate the function on one valid reading and one impossible one.

def to_fahrenheit(celsius):
    assert celsius >= -273.15, "Temperature below absolute zero"
    return celsius * 9 / 5 + 32

print(to_fahrenheit(37))
print(to_fahrenheit(-300))

Sample output:

98.6
Traceback (most recent call last):
  File "temp.py", line 6, in <module>
    print(to_fahrenheit(-300))
  File "temp.py", line 2, in to_fahrenheit
    assert celsius >= -273.15, "Temperature below absolute zero"
AssertionError: Temperature below absolute zero

Identifier description:

[Table:] Table 1.6 – identifiers used in the temperature conversion program | Identifier | Type | Purpose | |—|—|—| | to_fahrenheit | function | Converts a Celsius reading to Fahrenheit after checking it is possible | | celsius | float or int | Parameter holding the reading to be converted | [/Table]

Notes: the first call succeeded, so the assertion did nothing whatsoever and the converted value was printed. The second call never reached the return statement. Placing the check on the first line means the function refuses bad input before doing any work with it, which is why an assertion is normally written at the very top of a function body.

Reading a traceback that crosses a function

This traceback carries two File lines where earlier ones carried one, and the extra line is informative. Read from the top: execution was at line 6 of the file, in the main body of the program, which is what <module> means; that statement called to_fahrenheit, and inside it, at line 2, the assertion failed. The File lines therefore list the sequence of calls that led to the failure, in the order they were made, with the place where the trouble actually occurred at the bottom — which is what the words “most recent call last” in the header promise. In a program with functions calling functions, that list is often the quickest route to the real cause.

Two cautions belong with assert. First, an assertion says “this must be true, and if it is not, my program is broken” — it is a self-check, not a way of validating what a user types. A user typing 150 when asked for a mark has not broken your program; they have done something you should expect and handle. Second, assertions can be switched off when Python is run with the -O option, and switched-off assertions test nothing at all. For anything that must be checked in a finished program, use an explicit if and a raise.

Table 1.7 – the raise statement vs the assert statement | Feature | raise | assert | |—|—|—| | What is written | The exception to be raised, with an optional message | A condition that must be true, with an optional message | | When the exception occurs | Whenever the statement is reached | Only when the condition turns out to be false | | Which exception is raised | Any type you name | Always AssertionError | | Typical use | Signalling a situation the program is expected to meet, such as bad input | Checking an assumption the programmer believes cannot fail | | Can it be switched off | No | Yes, when Python is run with the -O option | | Usual position in the code | Inside an if that tests for the situation | On the first lines of a function, before any work is done |

Q. If assert only ever raises AssertionError, why not always write if and raise instead?

A. You could; the two are not rivals so much as different in intent. An assert is a claim about what must be true, written in one short line and read as documentation of the function’s requirements. A raise lets you choose the exception type, so that different situations arrive under different names and can be handled differently. Use assert for the assumptions, raise for the situations you expect.

Q3. Can a programmer deliberately make a program signal an error, even when Python itself sees nothing wrong? Why would anyone want to?

A. Yes — that is what the raise and assert statements are for. Python only knows what the language cannot do; it has no idea that 130 is an impossible mark or that −3 copies cannot sit on a shelf, because those are perfectly ordinary numbers. The programmer knows, and can raise an exception at exactly the point the rule is broken. The reason for doing so is that a wrong value which is allowed to travel on through a program becomes a wrong file, a wrong report and a problem nobody can trace. Raising an exception stops the wrong value at its source, names the trouble in the terms of the problem, and hands the decision about what to do next to the code that is best placed to make it.

Take the book issue program and change the rule: instead of refusing a request larger than the stock, refuse any request of zero or fewer copies, raising an exception with a message of your own. Test it with a request of 0 and then with a request of 3. Now rewrite the same check as a single assert statement, run both versions, and note the one difference in what appears on the last line of the traceback.

1.6 Handling Exceptions

Everything so far has been diagnosis. An exception is raised, the program stops, a traceback appears, and you read it to find out what went wrong. That is fine while you are developing a program at your own desk. It is useless once somebody else is using it.

Think of the person at a school library counter with twenty students in a queue. If the issue program collapses because a student’s roll number was typed with a stray letter, that person is left staring at a traceback about ValueError and has no idea what to do next. What they needed was a line saying “roll number must be digits only — please type it again”, and a program still running.

Writing the code that produces such a response is called exception handling.

NoteDefinition

Exception handling is the process of writing code in a program that deals with an exception when it is raised, so that a suitable message or action is produced and the program continues in a controlled way instead of terminating abnormally.

1.6.1 Need for Exception Handling

Exception handling is not a Python invention. C++, Java, Ruby and most other modern languages provide the same facility, in much the same shape, because every one of them ran into the same problem: a program that meets an unexpected situation has to do something better than stop.

Consider what the alternative would look like. You could try to test for every bad possibility before it happens.

months = int(input("Number of months: "))
if months == 0:
    print("Number of months cannot be zero")
else:
    print(6000 / months)

For division that works. But look at the very first line, which is the one most likely to fail: if the user types four, the conversion fails before any if of yours gets a chance to run. And you cannot easily test in advance whether a string can be converted to an integer — to find out for certain, you would have to examine it character by character, which means writing the work of int() all over again. Some failures simply cannot be detected until the operation is attempted.

There is a second difficulty, and it is about readability. A program guarded by tests for every possible mishap buries its actual purpose. Ten lines of real work end up wrapped in thirty lines of checking, and a reader can no longer see what the program is for.

Exception handling answers both difficulties, and the reasons are worth stating plainly:

  1. The risky operation is simply attempted. If it succeeds, nothing is lost; if it fails, the failure is caught. No advance test is needed for situations that can only be discovered by trying.
  2. Python sorts failures into distinct types, so a separate handler can be written for each one — a different message for a missing file, a bad number and an impossible division.
  3. The main logic of the program stays in one block and the error-handling code sits in another. The purpose of the program remains readable, because the checking is no longer tangled through it.
  4. Python keeps track of exactly where the error occurred, so the handler knows which operation failed without your having to record it.
  5. The same mechanism serves both built-in exceptions and the user-defined ones a programmer creates, so there is one way of dealing with errors rather than two.

1.6.2 Process of Handling Exception

Before writing a handler it helps to know what Python is actually doing when an exception is raised, because the machinery explains several rules that would otherwise look arbitrary.

When an error occurs, the interpreter creates an exception object. This object holds the information about the failure: what type of error it is, a description of it, the name of the file and the position in the program where it happened. Creating this object and handing it over so that it can be dealt with is called throwing an exception.

The object is handed to the runtime system — the machinery of interpreter and operating system that comes into action when a program is put to run and keeps it running. Its job now is to find code that can deal with this particular exception. Such a block of code is called an exception handler.

NoteDefinition

An exception handler is a block of code written to deal with a particular type of exception when it is raised.

The search is not random. The runtime system first looks in the function where the exception was raised. If that function contains no handler for this type, it looks in the function that called it; if that has none, it looks in the function which called that, and so on backwards along the chain of calls. This chain — the list of functions that are part-way through executing, each waiting for the one it called to finish — is the call stack. You met it in Section 1.5.2 without the name: the several File lines of a traceback are the call stack, printed out.

When a suitable handler is found, it is executed. This is called catching the exception, and the program continues from there. If the search reaches the bottom of the call stack without finding a handler anywhere, there is nothing more to be done: the program stops and the traceback is printed. Fig. 1.7 sets out the whole sequence.

Fig. 1.7

Fig. 1.7 – Exception handling mechanism across the call stack.

the exception handling mechanism Statement: when an exception is raised, execution of the current block is abandoned and the runtime system searches the call stack outwards from the point of failure for a handler matching the exception’s type; the first matching handler is executed and the program continues from it, and if none is found the program terminates with a traceback. In words: the error travels outwards from where it happened until it meets code that knows what to do about it. Why it holds: the exception is an object with a type, so a handler can be tested against it; and because a function that has called another is suspended waiting for it, the chain of suspended calls gives a natural, ordered set of places to look. Used for: keeping a program running through failures it can sensibly recover from, and reporting the ones it cannot in terms a user can understand. Watch out: statements between the point of failure and the handler are skipped entirely. Anything they were going to do — writing a total, closing a file — does not get done unless you arrange for it. Section 1.7 is about that arrangement.

Many books describe this search in terms of methods, a word borrowed from languages such as Java. In Python the ordinary word is function. The mechanism is the same either way: the search moves outwards from the innermost call to the outermost.

1.6.3 Catching Exceptions

An exception is caught by putting the statements that might fail inside a try block, and the code to deal with the failure inside an except block that follows it.

the try…except statement

try:
    statements in which an exception might occur
except exception_name:
    statements to be executed if that exception is raised

Parts: the try block holds the statements under suspicion — usually a small number of them; exception_name is the type of exception this handler deals with, written as one of the names from Table 1.4; the except block holds the response. Returns / Effect: if no exception is raised, the whole try block runs and the except block is skipped entirely. If the named exception is raised anywhere in the try block, the rest of that block is abandoned at once and the except block runs instead. Either way, execution then continues with the statement after the whole try … except construct. Example:

try:
    reading = int(input("Enter reading: "))
except ValueError:
    print("That was not a whole number")

Read the effect once more, because the two halves of it carry the whole idea. The except block is not part of the ordinary flow of the program — it runs only when something has gone wrong. And the try block is not resumed after the handler finishes; the statements it did not reach stay unreached.

class average with a guarded division Problem: read the total marks scored by a class and the number of students, and print the average. If the number of students is entered as zero, the program must print a message explaining the difficulty and still finish in an orderly way. Two runs are shown, one with a valid entry and one with zero students.

print("Class average calculator")
try:
    total = int(input("Total marks scored by the class: "))
    students = int(input("Number of students: "))
    print("Average marks:", total / students)
except ZeroDivisionError:
    print("There must be at least one student")
print("Calculation over")

Sample output:

Class average calculator
Total marks scored by the class: 308
Number of students: 4
Average marks: 77.0
Calculation over
Class average calculator
Total marks scored by the class: 308
Number of students: 0
There must be at least one student
Calculation over

Identifier description:

[Table:] Table 1.8 – identifiers used in the class average program | Identifier | Type | Purpose | |—|—|—| | total | int | Sum of the marks scored by the whole class | | students | int | Number of students the total is to be divided among | [/Table]

Notes: only the three statements that can fail are inside the try block; the two print() calls that frame the program sit outside it, because nothing about them is risky. Notice the last line of both runs. Calculation over is printed whether the division succeeded or not, because a caught exception does not end the program — it merely diverts it.

the class average program with a total of 308 and 0 students | Step | Statement executed | total | students | Printed | |—|—|—|—|—| | 1 | print("Class average calculator") | – | – | Class average calculator | | 2 | total = int(input(...)) | 308 | – | prompt shown; 308 typed | | 3 | students = int(input(...)) | 308 | 0 | prompt shown; 0 typed | | 4 | print("Average marks:", total / students) | 308 | 0 | nothing — ZeroDivisionError raised while working out the division | | 5 | print("There must be at least one student") | 308 | 0 | There must be at least one student | | 6 | print("Calculation over") | 308 | 0 | Calculation over |

Result: the output is the second of the two runs shown above. The trace shows the two things worth remembering — the print() at step 4 never printed anything, because the exception was raised while its argument was being worked out, and control jumped from the middle of the try block straight to the handler.

Q4. If only three lines of a long program are risky, must the whole program be rewritten to protect against a failure in those three lines?

A. No. That is precisely what the try block is for. You place inside it only the statements that might fail — three lines, or one — and leave the rest of the program exactly as it was, outside. The handler that deals with the failure goes in the except block beside it. The main logic stays in one piece and stays readable, and the error-handling code sits in its own block where a reader can see at a glance what it is for.

leaving the risky statement outside the try block What students write:

students = int(input("Number of students: "))
average = 308 / students
try:
    print("Average marks:", average)
except ZeroDivisionError:
    print("There must be at least one student")

What happens: entering 0 still stops the program with a traceback ending in ZeroDivisionError: division by zero, even though a handler for that very exception is sitting a few lines below. The fix: put the division itself inside the try block.

try:
    students = int(input("Number of students: "))
    average = 308 / students
    print("Average marks:", average)
except ZeroDivisionError:
    print("There must be at least one student")

A handler protects only the statements inside its own try block. The division on line 2 of the first version had already failed before the try block was ever reached.

1.6.4 Handling More Than One Exception

A single stretch of code can usually fail in more than one way, and the different failures deserve different replies. Python allows a try block to be followed by several except blocks, one for each type.

When an exception is raised, the runtime system compares its type with the name on the first except clause, then the second, and so on, until one matches. The first matching handler runs, and the remaining except blocks are ignored. If none of them matches, the exception is not caught here at all, and the program stops with a traceback exactly as though no handler had been written. Fig. 1.8 shows the search.

Fig. 1.8

Fig. 1.8 – Sequential matching of multiple except clauses.

budget planner with three kinds of failure guarded Problem: a small shop keeps the prices of four items in a list, where a price of 0 means the price has not yet been recorded. Read a budget and an item number, and print how many whole units of that item the budget will buy. The program must deal separately with an entry that is not a whole number, an item number outside the list, and an item whose price has not been recorded.

prices = [250, 180, 0, 320]
print("Budget planner")
try:
    budget = int(input("Enter budget in rupees: "))
    position = int(input("Enter item number (0 to 3): "))
    units = budget // prices[position]
    print("Units affordable:", units)
except ValueError:
    print("Please enter whole numbers only")
except IndexError:
    print("There is no item with that number")
except ZeroDivisionError:
    print("That item has no price recorded")
print("Planner closed")

Sample output:

Budget planner
Enter budget in rupees: 1000
Enter item number (0 to 3): 7
There is no item with that number
Planner closed
Budget planner
Enter budget in rupees: 1000
Enter item number (0 to 3): 2
That item has no price recorded
Planner closed

Identifier description:

[Table:] Table 1.9 – identifiers used in the budget planner | Identifier | Type | Purpose | |—|—|—| | prices | list of int | Price of each of the four items; 0 marks a price not yet recorded | | budget | int | Amount of money available, in rupees | | position | int | Number of the item chosen, used as an index into prices | | units | int | Whole units of the chosen item the budget will buy | [/Table]

Notes: all three failures come from the same three statements, which is why one try block serves them all. A run with a budget of 1000 and item number 1 raises nothing, so every except block is skipped and the program prints Units affordable: 5 before Planner closed; typing one thousand for the budget raises ValueError in the very first statement, so the remaining two are never attempted. The order of these three except clauses does not matter, because no one of these types is a kind of another — but where one exception type is a specific kind of another, as FileNotFoundError is of OSError, the specific one must be written first, or the general handler will catch it and the specific handler will never run.

An except clause with no name

Even four handlers cannot anticipate everything. Python therefore allows an except clause with no exception name at all, which catches anything that reaches it.

except ZeroDivisionError:
    print("That item has no price recorded")
except:
    print("Something unexpected went wrong")

Because it matches every type, such a clause must be written as the last one. Putting it earlier would make every clause after it unreachable, and Python refuses the arrangement outright.

writing the unnamed except before a named one What students write:

try:
    units = budget // prices[position]
except:
    print("Something unexpected went wrong")
except ZeroDivisionError:
    print("That item has no price recorded")

What happens: the file does not run at all, and Python reports

SyntaxError: default 'except:' must be last

The fix: move the unnamed clause below every named one. An unnamed except matches everything, so any handler placed after it could never be reached, and Python treats that as a mistake in the writing rather than letting it pass silently.

Q. If an unnamed except catches everything, why not use it every time and save the trouble of naming types?

A. Because it catches your own mistakes along with the user’s. Misspell a variable name inside the try block and the resulting NameError is swallowed too, so a plain typing error is reported to the user as “something unexpected went wrong” and you never see the traceback that would have told you where it was. It is also unhelpful to the user, who is told that something failed but not what to do differently. Name the types you expect, and keep the unnamed clause as a last resort.

1.6.5 The try…except…else Clause

There is often work that should be done only if the risky part succeeded — printing the result, writing the record, preparing the receipt. You could put those statements at the end of the try block, and many programs do. Python offers something better: an optional else block, written after all the except blocks, which runs only when the try block completed without raising any exception.

the try…except…else statement

try:
    statements in which an exception might occur
except exception_name:
    statements to be executed if that exception is raised
else:
    statements to be executed only if no exception was raised

Parts: the try and except blocks are as before; the else block holds the statements that belong to the successful case, and must be written after every except block. Returns / Effect: exactly one of the two happens — either an exception is raised and a matching handler runs, or no exception is raised and the else block runs. The else block is never executed when an exception has been caught.

fee receipt using try…except…else Problem: a school office reads a total fee and the number of months it is to be spread over, works out the monthly instalment and prepares a receipt. A non-numeric entry and a month count of zero must each produce their own message, and the receipt must be prepared only when the calculation actually succeeded. Two runs are shown.

print("Fee receipt")
try:
    amount = int(input("Fee amount: "))
    months = int(input("Number of months: "))
    monthly = amount / months
except ValueError:
    print("Enter whole numbers only")
except ZeroDivisionError:
    print("Number of months cannot be zero")
else:
    print("Monthly fee:", monthly)
    print("Receipt prepared")
print("Counter free")

Sample output:

Fee receipt
Fee amount: 6000
Number of months: 4
Monthly fee: 1500.0
Receipt prepared
Counter free
Fee receipt
Fee amount: 6000
Number of months: 0
Number of months cannot be zero
Counter free

Identifier description:

[Table:] Table 1.10 – identifiers used in the fee receipt program | Identifier | Type | Purpose | |—|—|—| | amount | int | Total fee to be paid | | months | int | Number of months the fee is spread over | | monthly | float | Instalment payable each month | [/Table]

Notes: in the second run no receipt is prepared, which is the correct behaviour — there was no instalment to print. The division produces a float, so the first run shows 1500.0 and not 1500. The try block now contains only the three statements that could fail, and every statement that assumes success sits in the else block.

Q. Why not simply put the two receipt lines at the end of the try block?

A. Because the handlers would then be guarding them too. If a statement in the successful part ever raised ValueError for some quite different reason, the user would be told “enter whole numbers only” — a message that has nothing to do with what actually failed. Keeping the try block down to the statements you are actually suspicious of means each handler answers for exactly the failure it was written for.

Practical link: this task exercises try, multiple except clauses and the else block, and is filed under Python programs in your practical report file. Write a program that reads a student’s roll number, marks in three subjects, and prints the roll number with the average of the three marks rounded to two decimal places. The program must reject a mark that is not a whole number and a mark outside 0 to 100, giving a different message for each, and must print the result only when all three marks were accepted. Test it with the marks 78, 65 and 90, and then with the unseen input 78, 65 and 190.

1.7 Finally Clause

Some work has to be done whatever else happens. A shopkeeper closes the cash register at the end of the day whether the day’s takings balanced or not. A student who signs out equipment from the laboratory signs the register back in whether the experiment worked or not. The task is not part of the success; it is part of leaving things tidy.

Programs have such work too, and Section 1.6.2 warned about it. When an exception is raised, the statements between the point of failure and the handler are skipped. If one of those skipped statements was closing a file, saving a total or releasing something the program had taken hold of, that job silently goes undone — and the program leaves a mess behind it.

Python’s answer is a fourth kind of block. A finally clause may be added to a try statement, and the statements inside it are executed in every case: whether an exception was raised or not, and whether it was caught or not. Code of this kind, which puts things back in order regardless of what happened, is called clean-up code.

NoteDefinition

The finally clause is an optional block of a try statement whose statements are always executed, whether or not an exception was raised in the try block, and whether or not any exception raised there was handled.

If it is used, finally is written at the end of the try statement — after all the except blocks and after the else block. That position matches its meaning: everything else has had its chance, and this runs last.

the complete try statement

try:
    statements in which an exception might occur
except exception_name:
    statements to be executed if that exception is raised
else:
    statements to be executed only if no exception was raised
finally:
    statements to be executed in every case

Parts: try holds the suspect statements; each except handles one type of exception; else holds the work that belongs to the successful case; finally holds the clean-up. Returns / Effect: except and else are alternatives — exactly one of them runs. finally is not an alternative to anything; it runs after whichever of them ran, and also when nothing matched at all. Example:

try:
    reading = int(input("Enter reading: "))
except ValueError:
    print("That was not a whole number")
finally:
    print("Reading attempted")

day-end sales entry with a guaranteed closing line Problem: at the end of the day a shop assistant enters the amount collected and the number of items sold, and the program prints the average price per item. Whatever happens, the program must print the closing line for the day’s register, because the register has to be closed even when the entry was faulty. Two runs are shown.

print("Day-end sales entry")
try:
    amount = int(input("Amount collected: "))
    items = int(input("Items sold: "))
    print("Average price per item:", amount / items)
except ZeroDivisionError:
    print("No items were sold today")
finally:
    print("Counter closed for the day")

Sample output:

Day-end sales entry
Amount collected: 4800
Items sold: 12
Average price per item: 400.0
Counter closed for the day
Day-end sales entry
Amount collected: 4800
Items sold: 0
No items were sold today
Counter closed for the day

Identifier description:

[Table:] Table 1.11 – identifiers used in the day-end sales program | Identifier | Type | Purpose | |—|—|—| | amount | int | Money collected during the day, in rupees | | items | int | Number of items sold during the day | [/Table]

Notes: the closing line appears in both runs, and appears last in both. In the first run it followed the successful average; in the second it followed the handler’s message. Nothing in the program decides whether to print it, which is the point — a finally block is not something the program chooses to run.

At this stage a fair question arises: the class average program in Section 1.6.3 printed Calculation over at the end without any finally clause at all, simply by putting the statement after the try construct. Why is a whole new kind of block needed?

The answer is that a statement written after the try construct runs only if execution actually reaches it. When an exception is caught, it does. When an exception is not caught, execution never gets that far — and that is exactly the case Section 1.7.1 examines.

Table 1.12 – the else clause vs the finally clause | Feature | else | finally | |—|—|—| | When it runs | Only when the try block raised no exception | In every case, without exception | | Purpose | The work that belongs to the successful case | Clean-up that must happen whatever the outcome | | Runs when an exception was caught | No | Yes | | Runs when an exception was not caught | No | Yes, before the program stops | | Position in the statement | After all except blocks | Last of all, after else | | Typical contents | Printing the result, saving the record | Closing a file, printing a closing entry |

Q. A file is opened inside a try block. Where should it be closed?

A. In the finally block. A file left open holds resources and may lose data that was written to it but not yet saved to disk, and the one moment you most need it closed is the moment something has gone wrong. Closing it in the else block would close it only after a successful run; closing it after the whole construct would miss the case where the exception was never caught. This is the commonest real use of finally, and you will use it throughout the work on files in the next chapter.

1.7.1 Recovering and Continuing with the finally Clause

Suppose the try block raises an exception for which no except clause has been written. From Section 1.6.4 you know what happens to the exception: it is not caught here, so the search moves outwards along the call stack, and if nothing anywhere handles it the program stops with a traceback.

What happens to the finally block in that case? It still runs — and it runs first, before the exception continues on its way.

laboratory issue with an unhandled entry error Problem: a laboratory assistant enters the number of beakers available and the number of student groups, and the program prints how many beakers each group gets. A group count of zero is handled. An entry that is not a whole number is deliberately left unhandled, so that you can see what the finally block does when nothing catches the exception. The run shown uses a word where a number was expected.

print("Lab equipment issue")
try:
    beakers = int(input("Beakers available: "))
    groups = int(input("Number of groups: "))
    print("Beakers per group:", beakers // groups)
except ZeroDivisionError:
    print("There must be at least one group")
finally:
    print("Register entry made")
print("Desk free")

Sample output:

Lab equipment issue
Beakers available: twenty
Register entry made
Traceback (most recent call last):
  File "lab.py", line 3, in <module>
    beakers = int(input("Beakers available: "))
ValueError: invalid literal for int() with base 10: 'twenty'

Identifier description:

[Table:] Table 1.13 – identifiers used in the laboratory issue program | Identifier | Type | Purpose | |—|—|—| | beakers | int | Number of beakers available in the laboratory | | groups | int | Number of student groups sharing them | [/Table]

Notes: read the order of that output carefully. Register entry made was printed, and only then did the traceback appear. The register entry — the clean-up — was made even though the program was on its way to stopping. And Desk free, the statement written after the whole construct, never ran at all, because execution never got that far.

So finally does not catch the exception and does not cancel it. Once the clean-up has been carried out, the exception continues to be raised, and Python looks for a handler further out: in an enclosing try statement, if the failure happened inside one, or otherwise in the default handler, which prints the traceback and ends the program. This is the difference between except and finally stated exactly — an except clause deals with an exception and the program carries on afterwards, while a finally clause merely gets some work done on the way past.

That behaviour is what makes finally trustworthy. If it could swallow an exception, you would have to think about what it might be hiding every time you used one. Because it cannot, you can put clean-up in it freely, knowing that the error will still be reported to whoever needs to see it.

There is a matching case worth stating for completeness. When the exception is caught by one of the except clauses, the finally block runs after that handler, and execution then continues with the statement after the whole construct in the ordinary way. In the sales program of Section 1.7, that is precisely what happened in the second run.

Fig. 1.9

Fig. 1.9 – Execution paths in a complete try statement.

expecting finally to deal with the exception What students write:

try:
    marks = int(input("Marks: "))
finally:
    print("Entry attempted")
print("Next student")

What happens: for an entry such as A1, the program prints Entry attempted and then stops with a traceback ending in ValueError. The line Next student is never printed, although the student expected the program to move on. The fix: add a handler; keep finally for the clean-up.

try:
    marks = int(input("Marks: "))
except ValueError:
    print("Marks must be a whole number")
finally:
    print("Entry attempted")
print("Next student")

A finally clause guarantees that something is done; it does not decide what to do about the failure. Only an except clause can do that, and without one the exception carries on after the clean-up.

Q5. If a program is going to stop anyway, can it still be made to finish some last piece of work — such as closing a file it opened — before it goes?

A. Yes, and that is exactly what the finally clause is for. Statements placed in a finally block are executed in every case — when the try block succeeded, when an exception was caught, and even when no handler was found and the program is about to stop. In that last case the clean-up runs first and the traceback appears afterwards, as the laboratory program above shows. This is why a file opened inside a try block is closed in the finally block: whatever else goes wrong, the file is not left open.

Take the laboratory issue program and add an except ValueError: clause with a message of your own. Predict, before running it, which lines will now appear when you type twenty at the first prompt — and in particular whether Desk free will be printed this time. Then run it and check. Finally, delete the finally block altogether, run it once more with twenty, and note exactly which line has disappeared from the output.

1.8 Summary

  1. A program can fail in three distinct ways. A syntax error is a statement not written according to the rules of Python, and is detected before execution begins. A run-time error occurs in a correctly written statement that cannot be carried out on the values it meets. A logical error runs to completion and prints a wrong answer, and only a human can detect it.

  2. Syntax errors are also called parsing errors, because they are found while Python is parsing the code — working out its structure — and parsing finishes before execution starts. Python reports the file and line, marks the place where it lost track, and names the error on the last line.

  3. In interactive mode each statement is checked and run on its own, so a faulty line does not disturb the statements already entered. In script mode the whole file is parsed first, so a syntax error anywhere in the file prevents every statement in it from running.

  4. Indentation is part of Python’s grammar, so wrong indentation is reported as IndentationError, which is a particular kind of SyntaxError.

  5. An exception is an error that occurs during the execution of a syntactically correct program and disrupts its normal flow. In Python an exception is an object representing the error, carrying its type and a description of it. When such an object is created and the normal flow is interrupted, the exception is said to have been raised.

  6. When an exception is raised and nothing deals with it, Python prints a traceback. It is read from the bottom upwards: the last line gives the exception name and a description, and the lines above give the file, the line number and the statement being executed. Where functions are involved, those lines list the sequence of calls that led to the failure, most recent last.

  7. An unhandled exception abandons the rest of the program. Every statement already executed has taken effect, and everything the program still had to do is left undone.

  8. SyntaxError is itself an exception, so every syntax error is an exception; but most exceptions are not syntax errors, because they are raised by programs whose code has already been accepted as correctly written. A syntax error cannot be handled inside its own program, since the program never begins; a run-time exception can be, because the program is alive when it occurs.

  9. Built-in exceptions are the exception types already defined in Python for commonly occurring errors. The ones met most often are SyntaxError, ValueError, IOError, KeyboardInterrupt, ImportError, EOFError, ZeroDivisionError, IndexError, NameError, IndentationError, TypeError and OverflowError.

  10. Some exception names denote a particular kind of a broader one: IndentationError is a kind of SyntaxError, FileNotFoundError is a kind of OSError — for which IOError is another name — and ModuleNotFoundError is a kind of ImportError.

  11. A programmer may also define exception types of their own to represent situations that are wrong for the problem though perfectly legal in Python. These are called user-defined exceptions.

  12. Raising an exception means signalling an error condition so that the normal flow is interrupted at that point. Once an exception is raised, no further statement in the current block is executed. Programmers raise exceptions deliberately with the raise and assert statements.

  13. The raise statement names the exception type to be raised and may carry a message, generally a string, which appears after the colon in the traceback. What is raised must be an exception object; a bare string cannot be raised.

  14. The assert statement tests a condition that the programmer believes must be true. If the condition is true nothing happens; if it is false, AssertionError is raised with the optional message attached. Assertions state a program’s assumptions and are usually written at the top of a function, but they can be switched off when Python is run with the -O option, so they are not a substitute for validating what a user types.

  15. Exception handling is the writing of code that deals with an exception when it is raised, so that the program produces a suitable message or action and continues in a controlled way instead of terminating abnormally. It is needed because some failures cannot be tested for in advance, and because handling keeps the error-handling code separate from the main logic of the program.

  16. When an error occurs, the interpreter creates an exception object holding the type, the description and the position of the error, and hands it to the runtime system; this is called throwing the exception. The runtime system searches for an exception handler, starting in the function where the exception was raised and moving outwards along the call stack — the chain of functions waiting for the ones they called. Executing a matching handler is called catching the exception. If the search reaches the bottom of the call stack without a match, the program stops and the traceback is printed.

  17. Statements that might fail are placed in a try block, and the response in an except block. If no exception is raised, the whole try block runs and the except block is skipped. If the named exception is raised, the rest of the try block is abandoned at once and the except block runs instead. A handler protects only the statements inside its own try block.

  18. One try block may be followed by several except blocks. The type raised is compared with each in turn and the first match runs, the rest being skipped; if none matches, the exception is not caught there. Where one exception type is a particular kind of another, the specific one must be written first.

  19. An except clause written with no exception name catches every type, and must therefore be the last clause. It is a last resort, because it also swallows the programmer’s own mistakes and tells the user nothing useful.

  20. An optional else block, written after all the except blocks, runs only when the try block raised no exception. It holds the work that belongs to the successful case, which keeps the try block down to the statements actually under suspicion.

  21. An optional finally block, written last of all, runs in every case — after a successful try block, after a handler has run, and even when no handler was found. It holds clean-up code, most commonly the closing of a file that was opened.

  22. A finally block does not catch or cancel an exception. When nothing has handled the exception, the clean-up is carried out first and the exception then continues to be raised, so the traceback appears afterwards and the statements following the whole construct are never reached.

1.9 Key Terms

  1. assert statement — a statement that tests a condition the programmer believes must be true, and raises AssertionError if it turns out to be false.

  2. built-in exception — an exception type already defined in Python for a commonly occurring kind of failure, raised automatically by the interpreter.

  3. call stack — the chain of functions that are part-way through executing, each waiting for the one it called to finish; the runtime system searches it outwards when looking for a handler.

  4. catching an exception — finding a handler that matches the raised exception and executing it, so that the program can continue in a controlled way.

  5. clean-up code — statements that put things back in order whatever the outcome, such as closing a file; in Python they are placed in a finally block.

  6. else clause — the optional block of a try statement that runs only when the try block raised no exception.

  7. EOL — end of line; the point at which a line of source code finishes, named in some older syntax error messages.

  8. except block — the block holding the code that deals with a particular type of exception raised in the try block before it.

  9. exception — an error that occurs during the execution of a syntactically correct program and disrupts its normal flow; in Python it is an object carrying the error’s type and description.

  10. exception handler — a block of code written to deal with a particular type of exception when it is raised.

  11. exception handling — the process of writing code that deals with an exception, so that the program produces a suitable message or action instead of terminating abnormally.

  12. exception object — the object the interpreter creates when an error occurs, holding the type of the error, a description of it and the position at which it happened.

  13. finally clause — the optional block of a try statement, written last of all, whose statements are executed in every case, whether or not an exception was raised and whether or not it was handled.

  14. IDLE — integrated development and learning environment; the editing and shell environment supplied with Python.

  15. logical error — an error in which the program runs to completion without complaint but computes the wrong thing, detectable only by a human checking the output.

  16. parsing — the reading and checking of source code to work out its structure, carried out by Python before execution begins.

  17. parsing error — another name for a syntax error, so called because it is found while the code is being parsed.

  18. raise statement — a statement that deliberately raises an exception of a named type, optionally carrying a message.

  19. raising an exception — signalling an error condition so that the normal flow of execution is interrupted at that point and control passes to code written to deal with it.

  20. run-time error — an error in a correctly written statement that cannot be carried out on the values it meets while the program is running.

  21. runtime system — the machinery of interpreter and operating system that comes into action when a program is put to run, and which searches for a handler when an exception is thrown.

  22. script mode — working by saving statements in a .py file and running the file, so that the whole file is parsed before any statement executes.

  23. shell mode — working statement by statement at the >>> prompt of the Python shell, each statement being checked and run as it is entered; also called interactive mode.

  24. syntax error — an error caused by writing a statement that does not follow the rules of the Python language, detected before execution begins.

  25. throwing an exception — creating the exception object and handing it to the runtime system so that a handler can be found for it.

  26. traceback — the block of text Python prints when an exception goes unhandled, read from the bottom upwards, giving the exception name and description and the sequence of calls that led to the failure.

  27. try block — the block holding the statements that are suspected of being able to raise an exception, and which the handlers after it protect.

  28. user-defined exception — an exception type created by the programmer to represent a situation that is wrong for the problem being solved although perfectly legal in Python.

1.10 Points to Ponder

  1. A syntax error and an unhandled exception both stop a program, yet only one of them can be dealt with from inside the program itself. What is it about the moment at which each is detected that gives a program the power to defend itself against one and not the other?

  2. Python could have chosen to report a failed operation by returning a special value — say −1 from a division that could not be done — instead of raising an exception and interrupting the program. Some older languages did exactly that. What would a programmer gain from such a design, and what would be lost?

  3. A finally block cannot cancel the exception it cleans up after. Is that a restriction on what finally can do, or is it the very reason finally can be trusted?

  4. An except clause with no name catches every kind of exception, and Python allows it while also giving you good reasons never to reach for it first. When is a language right to permit something it expects you to use sparingly, rather than forbidding it outright?

  5. Handling an exception lets a program carry on after a failure. But consider a program that was half-way through writing a record when the failure occurred. Is a program that continues always better than one that stops, and how would you decide which of the two a particular situation calls for?

  6. An assertion states something the programmer believes can never be false, and it can be switched off when the program is run with the -O option. If a check is worth writing at all, does it ever make sense to write it in a form that someone can turn off later?

1.11 Objective Questions

  1. Which of the following is detected before a program begins to execute? [1 mark]
    1. ZeroDivisionError
    2. IndentationError
    3. NameError
    4. ValueError
  2. Which block of a try statement is executed only when no exception has been raised? [1 mark]
    1. except
    2. else
    3. finally
    4. assert
  3. An except clause written without any exception name must be placed [1 mark]
    1. immediately after the try block
    2. before all the named except clauses
    3. after all the named except clauses
    4. after the finally block
  4. Which exception is raised when the statement age = int("12.5") is executed? [1 mark]
    1. TypeError
    2. ValueError
    3. SyntaxError
    4. IndexError
  5. Assertion (A): The statements in a finally block are executed even when the exception raised in the try block is not handled by any except clause. [1 mark] Reason (R): A finally block catches every exception that no other clause of the try statement has caught.
    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.
  6. Assertion (A): An error in the indentation of a program is reported before any statement of that program is executed. [1 mark] Reason (R): Indentation is part of the grammar of Python, so wrong indentation is found while the code is being parsed.
    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.
  7. Match each exception in Column A with the situation in Column B that raises it. [1 mark]
Column A Column B
a. EOFError i. A module named in an import statement cannot be brought in.
b. IndexError ii. A position used on a sequence lies outside its valid range.
c. NameError iii. input() reaches the end of the input with nothing to read.
d. ImportError iv. A name is used before any value has been given to it.
  1. Fill in the blank: the block of text Python prints when an exception is left unhandled, which is read from the bottom upwards, is called a ____________. [1 mark]

  2. State whether the following is true or false: the statements written after a complete try … except … finally construct are executed even when the exception raised was matched by none of the except clauses. [1 mark]

  3. Write the full form of each of the following abbreviations: (a) EOL (b) IDLE. [1 mark]

1.12 Check Your Concepts

  1. Define an exception, and give one example of a situation in a correctly written program that raises one. [2 marks]

  2. Name any four built-in exceptions of Python and state, in one line each, when they are raised. [2 marks]

  3. Differentiate, in two points, between throwing an exception and catching an exception. [2 marks]

  4. Differentiate between a syntax error and a logical error, giving one short example of each. [3 marks]

  5. Write the general syntax of the try … except … else statement, and state in one line what each of its three blocks is for. [3 marks]

  6. A single try block is followed by three except clauses. Explain how Python decides which of the three is executed, and state what happens when the exception raised matches none of them. [3 marks]

  7. Give reasons why clean-up work such as closing a file is placed in a finally block rather than written after the whole try construct. Support your answer by describing what happens in each of the two arrangements when an exception is raised for which no handler has been written. [4 marks]

  8. Write a function safe_divide(a, b) that returns the quotient of a and b, and returns the string "Division not possible" when b is zero. Use exception handling rather than an if statement, and explain in one line why the function still works when a is zero. [4 marks]

  9. A program reads a mark and must reject any value that does not lie between 0 and 100. Explain how this rejection can be written using a raise statement, and how it can be written using an assert statement. Then give two reasons for preferring raise in a program that will be used by somebody other than its author. [5 marks]

1.13 Output & Debugging

  1. Write the output produced by the following code. [2 marks]
print("Start")
try:
    values = [4, 8, 15]
    print(values[3])
except IndexError:
    print("Out of range")
finally:
    print("Done")
print("Stop")
  1. Write the output produced by the following code, and state which of the four blocks was skipped. [2 marks]
try:
    n = int("50")
    print(100 / n)
except ValueError:
    print("Bad number")
else:
    print("Computed")
finally:
    print("Closed")
  1. The following code does not run at all. Identify the single fault, state the message Python reports, and rewrite the code correctly. [2 marks]
try:
    total = int(input("Enter total: "))
    print(1000 // total)
except:
    print("Invalid entry")
except ValueError:
    print("Enter a whole number")
  1. Consider the following code. [3 marks]
marks = [55, 62, 70]
i = 0
while i <= len(marks):
    print(marks[i])
    i = i + 1
a. Name the exception it raises.
b. State how many values are printed before the exception is raised.
c. Correct the code so that it prints all three marks and raises nothing.
  1. Consider the following function. [2 marks]
def middle(data):
    return data[len(data) // 2]
a. State in one line what the function returns in general.
b. Name the exception raised by the call `middle([])`.

1.14 Case-Based Questions

  1. The counter clerk of a school library uses the program below to work out the fine on a book returned late. The fine is ₹2 for each day. [4 marks]
print("Library counter")
try:
    roll = int(input("Roll number: "))
    days = int(input("Days late: "))
    fine = days * 2
    print("Fine for roll", roll, "is", fine)
except ValueError:
    print("Entries must be whole numbers")
finally:
    print("Entry recorded")
a. Write the complete output produced when the clerk types `12A` at the first prompt. (2 marks)
b. The clerk wants the two statements that work out and print the fine to be executed only when both entries were accepted. Name the clause into which they should be moved, and give one reason for moving them. (1 mark)
c. The program accepts a negative number of days late, which is impossible. Name the statement you would use to reject such a value, and state where in the program it would be placed. (1 mark)
  1. A bus depot keeps the fare of each of its four routes in a list, in which a fare of 0 means that the fare for that route has not yet been fixed. A clerk enters a route number and the amount a passenger is carrying, and the program prints how many whole tickets can be bought. [5 marks]
fares = [15, 22, 0, 30]
route = int(input("Route number: "))
money = int(input("Amount: "))
print("Tickets:", money // fares[route])
a. Name the exception raised when the clerk enters route number 4, and the exception raised when the clerk enters route number 2. (2 marks)
b. Rewrite the program so that each of these two situations produces a message of its own, and so that the line `Counter closed` is printed in every case, including a run in which nothing goes wrong. (3 marks)

1.15 Programming Practice

  1. Write a complete program that reads a percentage entered by a user and prints Valid entry only when the value lies between 0 and 100. Use an assert statement to carry out the check. Submit the complete program and state the exception name that appears when 145 is entered. [2 marks]

  2. Write a complete program that reads a number and prints its square root, using the math module. A negative number must not stop the program; it must produce the message Square root of a negative number is not defined and the program must then print Calculation attempted before it ends. Submit the complete program, and show the output for the inputs 49 and −9. [3 marks]

  3. Write a function average(values) that returns the average of a list of numbers, and raises ValueError with a suitable message when the list handed to it is empty. Then write a short program that calls the function twice — once with the list [40, 55, 70] and once with an empty list — and handles the second call so that the program prints a message and finishes normally. Submit the function, the calling program and an identifier description table. [4 marks]

  4. A small shop wants a program that reads the name of an item, its opening stock and the quantity sold during the day, and prints the closing stock. The program must reject an entry that is not a whole number with one message, and reject a quantity sold that is larger than the opening stock with a different message, and must print Stock register updated in every case, whether the entries were accepted or not. Submit the complete program together with an identifier description table, and state one input you tested it with that is not mentioned in this question. [5 marks]