CH 3 · EXCEPTION HANDLING
Unit 1 · Computational Thinking and Programming

Chapter 3
Exception Handling

Your programs already crash. This chapter is about deciding what happens when they do — instead of letting Python decide.

1.1 Where we have got to

Chapter 1 gave you the names

You met ValueError, TypeError and KeyError as accidents — red boxes that ended the program. You learned to read a traceback and name the exception.

Chapter 2 gave you the structure

You learned to put code inside a def, pass it arguments and return a value. A function that crashes returns nothing at all — the caller never gets its answer.

This chapter

Those same errors, but caught. A crash becomes a message, a retry, or a sensible default — and the program keeps running.

Why it is worth a whole chapter

Every program from here on touches the outside world — a file that may not exist, a number typed by a user, a database that may be down. Outside-world code fails, and Chapter 4 onwards is entirely outside-world code.

1.2 How this chapter runs

Six stops

  1. Three kinds of error — and only one of them is catchable
  2. The built-in exceptions — seen live, not listed
  3. try / except — the basic guard
  4. Several handlers — and the dangerous shortcut
  5. else and finally — the two optional blocks
  6. raise and assert — throwing one deliberately

The rule for this chapter

You cannot handle what you cannot name

Every handler you write names an exception. So the first half of this chapter is spent deliberately breaking programs and reading what Python calls the wreckage.

Same colour code as always

Blue = what you wrote · Green = what it printed · Red = what went wrong. In this chapter red boxes are the subject, not the failure.

2.1 A program that will not even start

main.py1marks = 50
2if marks > 40
3    print("Pass")
What is missing, and what does Python say? 🤔
Traceback File "main.py", line 2 if marks > 40 ^ SyntaxError: expected ':'
Definition

Syntax error — a breach of the grammar of the language, detected by the interpreter before the program runs. Also called a parsing error.

Look at what is NOT in the output

Line 1 is a perfectly good statement, and it never ran. There is no Traceback (most recent call last): header either, because there is no execution to trace. Python read the whole file, refused it, and stopped.

So

A syntax error cannot be handled. try and except are useless against it — the file will not compile, so your handler does not exist either. You fix it; you do not catch it.

2.2 A program that starts, then dies

main.py1print("Report starting")
2marks = [90, 85, 70]
3n = 0
4print("Average:", sum(marks) / n)
5print("Report done")
How much of this runs? 🤔
OutputReport starting
…then the tracebackTraceback (most recent call last): File "main.py", line 4, in <module> print("Average:", sum(marks) / n) ~~~~~~~~~~~^~~ ZeroDivisionError: division by zero
Definition

Exception — an error detected during execution of a syntactically correct program. An exception is a Python object representing the error; when it occurs it is said to be raised.

The difference from §2.1, in one observation

Report starting did print. Lines 1–3 ran normally; line 4 raised; line 5 never got the chance. The program was grammatically perfect — it just could not do what it was told.

And this one IS catchable

Everything from §3 onwards exists to stop that red box appearing and print Report done anyway.

2.3 A program that runs perfectly, and lies

average.pymarks = [90, 85, 70]
average = sum(marks) / len(marks) * 100
print("Average:", average)
No error. Is the answer right? 🤔
OutputAverage: 8166.666666666667
Definition

Logical error — the program is grammatically correct and runs to completion, but produces the wrong result, because the logic the programmer wrote is not the logic they intended.

The most dangerous of the three

Python raises nothing. Nobody is warned. The mistake was the stray * 100 — the average of three marks out of 100 cannot be 8166. Only you can catch this, by checking that the answer is sensible.

2.4 The three, side by side

 Syntax errorExceptionLogical error
Detectedbefore runningwhile runningnever, by Python
Does any code run?NoYes, until the bad lineYes, all of it
Python complains?YesYesNo
Can try…except catch it?NoYesNo
Examplemissing :50 / 0* 100 too many
You fix it bycorrecting the grammarwriting a handlertesting the output
Only the middle column is what this chapter is about.
One honest caveat

SyntaxError is an exception class in Python, and NCERT says so. But it is raised at compile time, not while your code is running — so a try block in the same file can never catch it.

If SyntaxError is an exception, why can't I catch it?

Because of when it happens. Python reads your whole file and turns it into bytecode first; only then does it start executing line 1. A syntax error is found during that first pass — before your try statement has come into existence.

Your objection is right in one case: if the broken code is in a different file or a string, the compile happens later, during execution — and then it is catchable:
try: import broken
except SyntaxError: print("that file is broken")

What to write in the exam: "Syntax errors are detected by the interpreter before execution begins, so they cannot be handled by try…except; they must be corrected." That is the answer the marking scheme wants, and the exception in the box above is well outside the syllabus.

2.5 Check yourself

Question 1

Classify each as a syntax error, an exception, or a logical error.

  1. print("Total is " + 500)
  2. for i in range(5)  (no colon)
  3. A program that prints the largest number when asked for the smallest
  4. L = [1, 2, 3] then print(L[3])
  5. A loop written while i < 10 that never changes i
Are you ready for the answer? 🤔
Answer
#KindName / reason
1ExceptionTypeError — can only concatenate str to str
2Syntax errormissing colon; nothing runs
3Logical errorruns fine, answer is wrong
4ExceptionIndexError — list index out of range
5Logical erroran infinite loop is bad logic, not bad grammar

Number 5 catches people out. Python is perfectly happy to run a loop forever — that is exactly what you told it to do.

3.1 Four you have already met

main.py1a = 10
2b = 0
3print(a / b)
Traceback File "main.py", line 3, in <module> print(a / b) ~~^~~ ZeroDivisionError: division by zero
main.py1n = int("12a")
Traceback File "main.py", line 1, in <module> n = int("12a") ^^^^^^^^^^ ValueError: invalid literal for int() with base 10: '12a'
main.py1print("Total: " + 500)
Traceback File "main.py", line 1, in <module> print("Total: " + 500) ~~~~~~~~~~^~~~~ TypeError: can only concatenate str (not "int") to str
main.py1d = {"a": 1}
2print(d["b"])
Traceback File "main.py", line 2, in <module> print(d["b"]) ~^^^^^ KeyError: 'b'
The Traceback (most recent call last): line

It has been trimmed off these four boxes to fit them on one screen. It is always there — write it in the exam if you are asked to reproduce a traceback.

3.2 …and three more you will meet in Chapter 4

main.py1marks = 90
2print(total)
Traceback File "main.py", line 2, in <module> print(total) ^^^^^ NameError: name 'total' is not defined

A name that was never bound — usually a typo.

main.py1L = [10, 20, 30]
2print(L[3])
Traceback File "main.py", line 2, in <module> print(L[3]) ~^^^ IndexError: list index out of range

Valid indices are 0, 1, 2 — never len(L).

main.py1f = open("marks.txt", "r")
Traceback File "main.py", line 1, in <module> f = open("marks.txt", "r") ^^^^^^^^^^^^^^^^^^^^^^ FileNotFoundError: [Errno 2] No such file or directory: 'marks.txt'

The whole reason Chapter 4 needs this chapter.

Forward link

FileNotFoundError is a subclass of IOError, which is the name the NCERT table uses. Opening a file for reading is the single most common real-world exception in this syllabus, and in Chapter 4 you will wrap every open() in a handler.

3.3 The table to learn

ExceptionRaised when…Typical trigger
SyntaxErrorthe grammar of Python is brokenmissing :, unbalanced bracket
IndentationErrorindentation is wrong or inconsistenta stray space at the start of a line
NameErrora name is used before it is boundmisspelling a variable
TypeErroran operation gets the wrong type"a" + 5, len(5)
ValueErrorright type, unusable valueint("abc")
ZeroDivisionErrorthe divisor is zeroa / 0, a % 0
IndexErrora sequence index is out of rangeL[len(L)]
KeyErrora dictionary key does not existd["missing"]
IOError / FileNotFoundErrora file cannot be openedopen("nofile.txt")
ImportErrora module cannot be foundimport maths
EOFErrorinput() hits end of inputCtrl+D at a prompt
OverflowErrora numeric result is too largemath.exp(1000)
KeyboardInterruptthe user interrupts the programCtrl+C

3.4 TypeError or ValueError?

The one distinction the paper tests, every year.

TypeError

The argument is of the wrong kind of thing. No value of that type would have worked.

wrong TYPEint([1, 2])      # a list? never
"age: " + 18     # an int? never
len(100)         # a number has no length
ValueError

The argument is of the right kind of thing, but this particular value cannot be used.

wrong VALUEint("12a")       # a str is fine — "25" works
int("")          # a str, but empty
[1,2,3].index(9) # an int is fine — 2 works
The test that always works

Ask: “could some other value of this same type have succeeded?”
int("abc") — yes, int("25") works → ValueError.
int([1, 2]) — no, no list ever works → TypeError.

3.5 Check yourself

Question 2

Name the exception each line raises.

1print(10 / (5 - 5))
2print(int("seventeen"))
3T = (1, 2, 3); print(T[3])
4print(len(2024))
5D = {"x": 1}; print(D["y"])
6print(marks + 1)  # marks never assigned
Are you ready for the answer? 🤔
Answer
LineExceptionWhy
1ZeroDivisionErrorthe bracket evaluates to 0 first
2ValueErrora str is acceptable; this one is not convertible
3IndexErrorvalid indices are 0, 1, 2 — tuples index like lists
4TypeErrorno integer has a length
5KeyErrorthe key is absent
6NameErrorthe name was never bound

Line 1 is the trap: students write ValueError because they see the subtraction. Python evaluates 5 - 5 to 0 perfectly happily — the failure is the division.

4.1 The problem, stated exactly

no guard — main.pyprint("Start")
a = 50
b = 0
print(a / b)
print("End")
OutputStart
…then the tracebackTraceback (most recent call last): File "main.py", line 4, in <module> print(a / b) ~~^~~ ZeroDivisionError: division by zero
guardedprint("Start")
a = 50
b = 0
try:
    print(a / b)
except ZeroDivisionError:
    print("Cannot divide by zero")
print("End")
OutputStart Cannot divide by zero End
Read what changed

Same bug, same division by zero. But now End printed, the exit was clean, and the user got a sentence instead of a traceback. Exception handling does not fix the error — it decides what the error costs.

Definition

Exception handling — writing code that anticipates a runtime error, catches the exception when it is raised, and takes a defined action, so that the program does not terminate abnormally.

4.2 What Python actually does

a statement fails exception object built is there a matching except block? handler runs program carries on program stops traceback printed YES NO The remaining statements in the try block are abandoned either way.
Raising an exception is throwing; running a matching handler is catching.
Vocabulary the paper uses

Throwing — creating the exception object and handing it to the runtime system.
Catching — finding and executing a handler that matches it.
Call stack — the chain of function calls the runtime searches, innermost first, looking for that handler.

4.3 The anatomy of the statement

syntaxtry:
    # statements that might fail
    # — the SUSPECT code
except ExceptionName:
    # what to do if that
    # exception is raised
Both blocks are indented

except lines up with try, not inside it. Getting that wrong is an IndentationError before you even test the logic.

try

Holds the suspect statements — the ones you believe might raise. Keep it short: only the lines that can actually fail.

except

Holds the handler — what to do when that named exception is raised. It runs only if the exception occurs, and only if the name matches.

A try with no except is a SyntaxError

Every try must be followed by at least one except or a finally. It cannot stand alone.

4.4 The rest of the try block is abandoned

skip.pytry:
    print("A")
    x = 10 / 0
    print("B")
except ZeroDivisionError:
    print("C")
print("D")
Does B print? 🤔
OutputA C D
B never prints

The moment a statement inside try raises, control leaves the block immediately. Everything below the failing line — the amber line here — is skipped, whether or not it would have worked.

Why this makes short try blocks a rule, not a style

If you wrap forty lines in one try, a failure on line 3 silently skips the other thirty-seven — and your handler cannot tell which of them ran. Put only the risky statement inside.

4.5 The board's favourite example

divide.pyprint("Marks calculator")
try:
    numerator = 50
    denom = int(input("Enter the denominator: "))
    quotient = numerator / denom
    print("Division performed successfully")
except ZeroDivisionError:
    print("Denominator as ZERO is not allowed")
print("OUTSIDE the try..except block")
Two runs: the user types 5, then 0. 🤔
Run 1 — user types 5Marks calculator Enter the denominator: 5 Division performed successfully OUTSIDE the try..except block
Run 2 — user types 0Marks calculator Enter the denominator: 0 Denominator as ZERO is not allowed OUTSIDE the try..except block
Compare the two

In run 1 the except block was skipped entirely. In run 2 "Division performed successfully" was skipped instead. The last line printed in both — it is outside the statement.

4.6 Check yourself

Question 3

Predict the exact output.

L = [1, 2, 3]
try:
    print(L[1])
    print(L[5])
    print(L[2])
except IndexError:
    print("Index problem")
print("Bye")
Are you ready for the answer? 🤔
Answer
Output2 Index problem Bye

L[1] succeeded and printed 2. L[5] raised, so the handler ran. L[2] was never reached — even though it would have worked perfectly. Then execution resumed after the statement.

5.1 Two things can go wrong

divide.pydef divide(a, b):
    try:
        return a / b
    except ZeroDivisionError:
        return "Zero not allowed"
    except TypeError:
        return "Numbers only"

print(divide(50, 5))
print(divide(50, 0))
print(divide(50, "x"))
Three lines. Mind the first one. 🤔
Output10.0 Zero not allowed Numbers only
The rule

One try may be followed by several except blocks. Python compares the raised exception against each in turn, top to bottom, and runs the first that matches. The rest are skipped.

Why the first line is 10.0 and not 10

Chapter 1, §3.1 — / always returns a float. Exception handling changed nothing about that.

5.2 Catching the exception object

asobj.pyvalues = ["8", "0", "abc"]

for v in values:
    try:
        print(100 / int(v))
    except ZeroDivisionError as e:
        print("ZeroDivisionError ->", e)
    except ValueError as e:
        print("ValueError ->", e)
Three lines. 🤔
Output12.5 ZeroDivisionError -> division by zero ValueError -> invalid literal for int() with base 10: 'abc'
What as e gives you

e is the exception object itself. Printing it gives the message text — the part after the colon in a traceback. Chapter 1 said an exception is an object; this is where you finally hold one.

Which exception hit which line

int("0") succeeded and gave 0, so the failure was the division. int("abc") failed before any division was attempted. Two different statements on one line, two different exceptions.

5.3 The catch-all except

bare.pyvalues = ["8", "0", "abc"]

for v in values:
    try:
        print(100 / int(v))
    except ZeroDivisionError:
        print("Denominator is zero")
    except:
        print("Some other error")
What catches the "abc"? 🤔
Output12.5 Denominator is zero Some other error
Definition

An except: with no exception name catches every exception. It is the safety net for the ones you did not anticipate, and it must be the last clause.

WRONG — main.pytry:
    x = 10 / 0
except:
    print("caught")
except ZeroDivisionError:
    print("never")
Traceback File "main.py", line 3 except: ^^^^^^^ SyntaxError: default 'except:' must be last

Python refuses outright — a bare except before a named one would make the named one unreachable.

5.4 Why a bare except is dangerous

It catches things you did not mean to catch

A bare except swallows NameError from your typo, KeyboardInterrupt when the user presses Ctrl+C, and every bug you have not found yet — and prints your cheerful message instead of telling you what really happened.

hides a typotry:
    total = 50 / dnom   # typo: denom
except:
    print("Zero not allowed")
OutputZero not allowed

A completely wrong diagnosis. The real error was NameError, and you will hunt for it for an hour.

honesttry:
    total = 50 / dnom
except ZeroDivisionError:
    print("Zero not allowed")
TracebackNameError: name 'dnom' is not defined

The bug surfaces immediately, because the handler did not match.

Board position

NCERT teaches the bare except as the final clause, and you should be able to write it. Name your exceptions wherever you can, and keep the bare one as a genuine last resort — say that in a written answer and you have shown judgement, not just recall.

5.5 Check yourself

Question 4

What is the output, and why does the third call behave differently?

def safe(L, i):
    try:
        return L[i] * 2
    except IndexError:
        return "bad index"

print(safe([1, 2, 3], 1))
print(safe([1, 2, 3], 7))
print(safe([1, 2, 3], "x"))
Are you ready for the answer? 🤔
Answer
First two lines print4 bad index
Then the third call crashesTypeError: list indices must be integers or slices, not str

Only IndexError is handled. L["x"] raises TypeError, no except clause matches, so the exception propagates out of the function and terminates the program. An unmatched handler is no handler at all.

Challenge

Add one line so that all three calls print something sensible, without hiding real bugs.

Answer
except TypeError:
    return "index must be an integer"

Adding except TypeError: is better than except: — it names the thing you expected, so a genuine bug elsewhere still surfaces.

6.1 else — the block that runs when nothing went wrong

withelse.pydef divide(a, b):
    try:
        q = a / b
    except ZeroDivisionError:
        print("Zero not allowed")
    else:
        print("Quotient is", q)

divide(50, 5)
divide(50, 0)
Two calls, how many lines out? 🤔
OutputQuotient is 10.0 Zero not allowed
Definition

else — an optional block after all the except clauses. It runs only if the try block completed without raising. If any exception occurred, else is skipped entirely.

Why not just put that line inside try?

Because then it too would be guarded. If print itself raised, your except ZeroDivisionError would be pointed at the wrong statement. else holds the code that depends on the try succeeding but is not itself suspect. Keep the try block to the risky line only.

6.2 finally — the block that always runs

withfinally.pydef divide(a, b):
    try:
        q = a / b
    except ZeroDivisionError:
        print("Zero not allowed")
    else:
        print("Quotient is", q)
    finally:
        print("-- attempt over --")

divide(50, 5)
divide(50, 0)
Four lines now. In what order? 🤔
OutputQuotient is 10.0 -- attempt over -- Zero not allowed -- attempt over --
Definition

finally — an optional block that is executed always, whether or not an exception was raised and whether or not it was handled. If present it must be last, after every except and after else.

Forward link — this is what it is for

In Chapter 4 you will write f = open(...) in try and f.close() in finally, so the file is closed even if reading it blew up. A file left open is the classic 1-mark deduction.

6.3 The complete statement

the full skeletontry:
    # risky statements
except FirstError:
    # runs if FirstError raised
except SecondError:
    # runs if SecondError raised
except:
    # runs for anything else
else:
    # runs if NOTHING was raised
finally:
    # runs in every case
The order is fixed

tryexcepts → elsefinally. Putting else after finally, or a bare except before a named one, is a SyntaxError.

BlockOptional?Runs when
tryrequiredalways entered
exceptsee noteits exception was raised
elseoptionalno exception was raised
finallyoptionalalways, last
Note

A try needs at least one except or a finally. else cannot appear without at least one except.

One sentence for each, for the paper

try — the code that may fail.
except — what to do if it did.
else — what to do if it did not.
finally — the tidying up, either way.

6.4 Which blocks run — all three cases at once

cases.pydef risky(v):
    try:
        print("try   :", 100 / int(v))
    except ZeroDivisionError:
        print("except: zero")
    finally:
        print("finally: always")

risky("4")
print("---")
risky("0")
print("---")
risky("abc")
print("never printed")
Only ZeroDivisionError is handled. What happens on the amber line? 🤔
Outputtry : 25.0 finally: always --- except: zero finally: always --- finally: always
then the tracebackTraceback (most recent call last): File "main.py", line 13, in <module> risky("abc") File "main.py", line 3, in risky print("try :", 100 / int(v)) ^^^^^^ ValueError: invalid literal for int() with base 10: 'abc'
Two things happened on the third call

finally ran even though nothing handled the exception.
Then the exception was re-raised and killed the program, so "never printed" never printed. finally does not swallow an exception — it only guarantees the tidying up happens first.

Read the traceback — it has two frames

The bottom frame is where the exception was raised (risky, line 3); the frame above it is who called that (<module>, line 13). That chain is the call stack the runtime searched, innermost first, looking for a handler. This is exactly the search in §4.2.

6.5 The master table

Scenario try statements matching except else finally Program after
No exception all runskipped runsruns continues
Exception, handled stops at the failureruns skippedruns continues
Exception, not handled stops at the failurenone matches skippedruns terminates with a traceback
Three rows. If you can reproduce this table, you can answer any "what is the output" question in this chapter.
Read the finally column

It is the only column that says runs three times out of three. That is the whole definition, and it is the one-mark answer.

6.6 The full board program

full.pyprint("Marks calculator")
try:
    numerator = 50
    denom = int(input("Enter the denominator: "))
    quotient = numerator / denom
    print("Division performed successfully")
except ZeroDivisionError:
    print("Denominator as ZERO is not allowed")
except ValueError:
    print("Only INTEGERS should be entered")
else:
    print("The result of division operation is", quotient)
finally:
    print("OVER AND OUT")
Three runs: 5, then 0, then abc. 🤔
Run 1 — user types 5Marks calculator Enter the denominator: 5 Division performed successfully The result of division operation is 10.0 OVER AND OUT
Run 2 — user types 0Marks calculator Enter the denominator: 0 Denominator as ZERO is not allowed OVER AND OUT
Run 3 — user types abcMarks calculator Enter the denominator: abc Only INTEGERS should be entered OVER AND OUT
Three details worth a mark each

OVER AND OUT appears in all three runs.
"Division performed successfully" appears only in run 1 — in runs 2 and 3 the try block was abandoned before reaching it.
The result is 10.0, not 10.

6.7 Check yourself

Question 5

Predict the exact output — and note the order carefully.

def f(x):
    try:
        return 10 / x
    except ZeroDivisionError:
        return -1
    finally:
        print("in finally", x)

print(f(2))
print(f(0))
Are you ready for the answer? 🤔
Answer
Outputin finally 2 5.0 in finally 0 -1

The order is the point. finally runs before the function actually hands its value back — so in finally 2 prints before 5.0 does. A return inside try cannot skip finally; nothing can.

Challenge

If finally contained return 99 instead of a print, what would f(2) give?

Answer

99. A return in finally replaces the one from try — and would swallow an unhandled exception too. It is legal, it is confusing, and you should never write it. Knowing why it is a bad idea is worth more than knowing that it works.

7.1 Throwing one on purpose

So far Python raised the exceptions. Now you do.

main.py1age = -5
2if age < 0:
3    raise ValueError("Age cannot be negative")
4print("Age accepted")
Does line 4 run? 🤔
TracebackTraceback (most recent call last): File "main.py", line 3, in <module> raise ValueError("Age cannot be negative") ValueError: Age cannot be negative
Definition

raise — forcibly throws an exception.
Syntax: raise ExceptionName("message")
The message is optional and is shown after the colon in the traceback.

Note two things

"Age accepted" never printed — once an exception is raised, no further statement in that block runs.
The message in the traceback is yours. You chose both the exception type and the wording.

7.2 Why you would ever want to

validate.pydef set_age(age):
    if age < 0:
        raise ValueError("Age cannot be negative")
    return age

for a in [21, -5]:
    try:
        print("Accepted:", set_age(a))
    except ValueError as e:
        print("Rejected:", e)
Two lines. 🤔
OutputAccepted: 21 Rejected: Age cannot be negative
The division of labour

The function knows what is invalid, but not what to do about it. The caller knows what to do, but not how to check. raise lets the function report the problem, and except lets the caller decide the response.

Compare with returning an error string

return "error" can be silently ignored by the caller, and it pollutes the return type — set_age() would sometimes give an int and sometimes a string. An exception cannot be ignored: either the caller handles it or the program stops.

7.3 assert — a check you can state in one line

main.py1def negativecheck(number):
2    assert number >= 0, "OOPS... Negative Number"
3    print(number * number)
4
5negativecheck(100)
6negativecheck(-350)
One number prints, one does not. 🤔
Output10000
then the tracebackTraceback (most recent call last): File "main.py", line 6, in <module> negativecheck(-350) File "main.py", line 2, in negativecheck assert number >= 0, "OOPS... Negative Number" ^^^^^^^^^^^ AssertionError: OOPS... Negative Number
Definition

assert — tests an expression. If it is False, an AssertionError is raised with the given message; if True, nothing happens at all.
Syntax: assert expression, message

The comma is not optional punctuation

assert (number >= 0, "message") — with brackets round both — is always True, because a non-empty tuple is truthy, so the check silently never fires. Write assert number >= 0, "message".

7.4 raise or assert?

raise

For conditions you expect to happen in normal use — bad user input, a missing record, a negative age typed into a form. You choose the exception type, so the caller can handle it precisely.

expectedif age < 0:
    raise ValueError("Age negative")
assert

For conditions that should be impossible if your program is correct — a sanity check on your own logic. Always raises AssertionError, so the caller cannot handle it precisely.

should be impossibleassert total >= 0, "total went negative!"
One practical warning

Every assert in a file is removed when Python is run with the -O (optimise) flag. So an assert must never be the only thing validating real user input — use raise for that. NCERT does not press this point, but it is the reason professionals use them differently.

One line for the paper

raise throws any exception you name; assert tests a condition and throws AssertionError if it is false.

7.5 Check yourself

Question 6

Predict the output, including the traceback if there is one.

marks = {"Riya": 88, "Aarav": 91}

def show(name):
    try:
        print(name, "scored", marks[name])
    except KeyError:
        print(name, "is not in the list")
        raise

show("Riya")
show("Kabir")
What does a bare raise inside an except do? 🤔
Answer
OutputRiya scored 88 Kabir is not in the list
thenTraceback (most recent call last): File "main.py", line 11, in <module> show("Kabir") File "main.py", line 5, in show print(name, "scored", marks[name]) ~~~~~^^^^^^ KeyError: 'Kabir'

A bare raise inside an except block re-raises the exception it just caught, unchanged. It is how you log a problem and still let it travel up to whoever should really deal with it — note that the traceback still points at line 5, the original failure, not at the raise.

8.1 Everything on one screen

The three kinds of error

syntax error — before running, not catchable exception — while running, catchable logical error — never reported

The exceptions

ZeroDivisionError ValueError — right type, bad value TypeError — wrong type NameError IndexError KeyError FileNotFoundError / IOError ImportError EOFError AssertionError

The statement

try — the risky lines only except Name — runs if that was raised except Name as e — gives you the object bare except — must be last else — runs if nothing was raised finally — always runs

Throwing your own

raise ValueError("msg") bare raise — re-raise what you caught assert cond, "msg" → AssertionError
The two sentences worth the most marks

An exception is a Python object representing a runtime error; when the error occurs the exception is said to be raised.
The finally block executes whether or not an exception occurred, and whether or not it was handled.

8.2 Exam question 1 — find the output

3 marks

Write the output of the following code.

L = [1, 2, 3]
try:
    print(L[1])
    print(L[5])
    print(L[2])
except IndexError:
    print("Index problem")
except:
    print("Other problem")
finally:
    print("Done")
print("Bye")
Are you ready for the answer? 🤔
Answer
Output2 Index problem Done Bye
LineWhat happened
print(L[1])printed 2
print(L[5])raised IndexError — try block abandoned
print(L[2])never reached
first exceptmatched, so the bare one was skipped
finallyran, as it always does

8.3 Exam question 2 — find the output

3 marks

Write the output. Six lines.

D = {"a": 1, "b": 2}
for k in ["a", "c", "b"]:
    try:
        print(k, D[k])
    except KeyError:
        print(k, "missing")
    else:
        print(k, "found")
Are you ready for the answer? 🤔
Answer
Outputa 1 a found c missing b 2 b found

Five lines, not six. That is the trap. For "c" the except ran, so the else was skipped — one line instead of two. else and except are mutually exclusive: never both, on any single pass.

8.4 Exam question 3 — write the program

3 marks

Write a program that reads two numbers from the user and prints their quotient. It must handle a non-numeric entry and a zero divisor separately, print the result only when the division succeeded, and print "Thank you" in every case.

All four blocks. Write it fully. 🤔
Answer
quotient.pytry:
    a = int(input("Enter numerator: "))
    b = int(input("Enter denominator: "))
    q = a / b
except ValueError:
    print("Please enter whole numbers only")
except ZeroDivisionError:
    print("Denominator cannot be zero")
else:
    print("Quotient =", q)
finally:
    print("Thank you")
Run — 50 and 4Enter numerator: 50 Enter denominator: 4 Quotient = 12.5 Thank you
Run — 50 and 0Enter numerator: 50 Enter denominator: 0 Denominator cannot be zero Thank you
Where the three marks are

two separately named except blocks
the result in else, not in try — because the question said "only when the division succeeded"
"Thank you" in finally — because the question said "in every case"

8.5 Exam question 4 — two-mark theory

2 + 2 marks
  1. Differentiate between a syntax error and an exception, with one example of each.
  2. What is the purpose of the finally block? How is it different from else?
Are you ready for the answer? 🤔
Answer 1

A syntax error is a violation of the grammar of Python. It is detected by the interpreter before execution begins, so no part of the program runs, and it cannot be handled by try…except — it must be corrected. Example: if x > 5 written without a colon.

An exception is an error detected during execution of a syntactically correct program. Statements before it have already run, and it can be handled. Example: int("abc") raising ValueError.

Answer 2

The finally block contains code that must run whatever happens — typically releasing a resource, such as closing a file. It executes whether or not an exception was raised, and whether or not it was handled.

The else block runs only when no exception was raised. So on a run where an exception occurs, else is skipped but finally still runs.

8.6 One last challenge

Challenge

Predict the exact output. Every idea in this chapter is in here somewhere.

def check(v):
    try:
        n = int(v)
        assert n > 0, "must be positive"
        return 100 // n
    except ValueError:
        return "not a number"
    except AssertionError as e:
        return str(e)
    finally:
        print("checked", v)

for item in ["4", "0", "xy"]:
    print(check(item))
Six lines. Mind the order. 🤔
Answer
Outputchecked 4 25 checked 0 must be positive checked xy not a number

"4" — converts, passes the assert, returns 25. finally prints before the value is handed back.
"0" — converts fine, so no ValueError; the assert fails, and str(e) gives the message you supplied. The division by zero never happens — the assert stopped it.
"xy"int() raises ValueError immediately.

End of Chapter 3

Next: File Handling

Everything so far has lived and died inside one run of the program. Chapter 4 makes data survive — text files, binary files with pickle, and CSV. It is also where every single thing you learned here gets used.

Carry these three forward

open() on a missing file raises FileNotFoundError — every read gets a handler.
f.close() belongs in finally, so the file is closed even when reading it fails.
A file converted with int() line by line will meet ValueError the first time a line has a stray space.