Class 12Computer Science · Programming with PythonFull chapter

Exception Handling

The whole chapter in one place — read it, then test yourself. Clear notes, a reference sheet, a practice quiz, and worked NCERT solutions & PYQs.

Errors, Exceptions and the Traceback

Quick answer A syntax error stops your program before a single line runs, while an exception is a runtime failure that Python raises as an object mid-execution — and only the second kind can be handled.

Every Python program can fail in two completely different ways, and the board expects you to tell them apart in one line.

A syntax error is a grammar mistake. Python reads and translates your whole file first; if the grammar is broken it refuses to start. Save this as syntaxdemo.py:

print("Line 1 runs fine")
if marks > 40
    print("Pass")

Running it produces no program output at all:

  File "syntaxdemo.py", line 2
    if marks > 40
                 ^
SyntaxError: expected ':'

Look very carefully at what is missing. The words Line 1 runs fine never appeared. That print on line 1 is perfectly valid Python, but it never ran, because the file never finished compiling. That is the fingerprint of a syntax error, and no amount of try and except can rescue it. You fix the missing colon and move on.

An exception is a different animal. The grammar is fine, the program starts, lines execute — and then it dies part-way through because of the actual values it met. Save this as marks.py:

marks = 450
total = 0
print("Percentage:", marks / total)
print("This line never runs")

The real output:

Traceback (most recent call last):
  File "marks.py", line 3, in 
    print("Percentage:", marks / total)
                         ~~~~~~^~~~~~~
ZeroDivisionError: division by zero

Nothing is wrong with that code as text. Divide 450 by 5 and it works fine. It only broke because total happened to hold 0 at that moment. That is exactly why exceptions are worth handling: the same correct code is safe with good data and fatal with bad data, and bad data is normal — a user types ninety instead of 90, a file has been renamed, a roll number is missing from a dictionary.

How to read a traceback. Students waste minutes reading it top-down. Read it bottom-up:

  1. The last line is the answer. It gives the exception class (ZeroDivisionError) and the reason (division by zero).
  2. The line above it shows the exact statement, with ~~~^~~~ markers pointing at the sub-expression that blew up. Here the markers sit under marks / total, not under the whole print.
  3. The File "...", line 3 line tells you where to go and fix it.

The words Traceback (most recent call last) at the top mean the list is oldest-call-first, so the place that actually failed is at the bottom. Everything above the last line is context.

Exceptions are objects, not just messages. Each one belongs to a built-in class, and it is that class name you will write after except. Every message in the table below was copied from an actual run — none of it is paraphrased:

Exception classCode that raises itMessage Python printed
ZeroDivisionError10 / 0division by zero
ValueErrorint("abc")invalid literal for int() with base 10: 'abc'
TypeError"Rs " + 500can only concatenate str (not "int") to str
IndexError[10, 20, 30][5]list index out of range
KeyError{"Aarav": 88}["Diya"]'Diya'
FileNotFoundErroropen("fees.txt")[Errno 2] No such file or directory: 'fees.txt'
NameErrortotl_marksname 'totl_marks' is not defined
AttributeError"88".upperr()'str' object has no attribute 'upperr'

Two of these are worth a second look because students confuse them constantly. ValueError means the type was acceptable but the value was not — int() is happy to take a string, it just cannot make a number out of "abc". TypeError means the type itself was wrong for the operation, like trying to glue an int onto a str.

Also note KeyError versus IndexError. A missing dictionary key gives KeyError; an out-of-range list or string position gives IndexError. Writing the wrong one in an except clause means your handler silently never fires.

Traceback last line ExceptionClass: reason Always read this line first — it names the class you must write after except, and why it fired.
ZeroDivisionError 10 / 0 Message: 'division by zero'. Also raised by // and % with a zero right operand, but the wording differs: 10 // 0 says 'integer division or modulo by zero' and 10 % 0 says 'integer modulo by zero'.
ValueError int('abc') Message: "invalid literal for int() with base 10: 'abc'". The type was fine, the value was not.
TypeError 'Rs ' + 500 Message: 'can only concatenate str (not "int") to str'. Wrong type for the operator.
KeyError {'Aarav': 88}['Diya'] Message is just the missing key: 'Diya'. Dictionaries only — a bad list position gives IndexError.
FileNotFoundError open('fees.txt') Message: "[Errno 2] No such file or directory: 'fees.txt'". A subclass of OSError, and IOError is only another name for OSError.
Remember
  • A syntax error is caught while Python is translating the file, so the program never starts — proven by the fact that print("Line 1 runs fine") produced no output before the SyntaxError. It cannot be handled with try-except; you fix the code.
  • An exception happens at runtime, after the program has started, and depends on the actual data — marks / total is fine until total is 0.
  • Read a traceback from the bottom up: the last line names the exception class and the reason, and the ~~~^~~~ markers point at the exact sub-expression that failed.
  • Every exception is an object of a built-in class, and that class name is what you write after except — get it wrong and your handler never runs.
  • ValueError = right type, impossible value (int("abc")). TypeError = wrong type for the operation ("Rs " + 500). KeyError is for dictionaries, IndexError for lists and strings.

try and except

Quick answer Put the risky statements in try and the recovery plan in except, always naming the specific exception class, and remember that the moment try fails the rest of try is abandoned.

The whole idea is one sentence: put the statements that might fail inside try, and put the recovery plan inside except. The general form is

try:
    risky statements
except ExceptionClass:
    what to do if that exception happens

Here is the divide-by-zero program from the last section, now handled:

marks = 450
subjects = 0

try:
    print("Starting calculation")
    average = marks / subjects
    print("Average is", average)
except ZeroDivisionError:
    print("Error: number of subjects cannot be zero")

print("Program continues normally")

Real output:

Starting calculation
Error: number of subjects cannot be zero
Program continues normally

Three things to notice, and all three are commonly examined.

First, Starting calculation did print. The statements in try before the failure run normally — try is not a rehearsal.

Second, Average is did not print. The instant a statement inside try raises, Python abandons the whole rest of the try block and jumps to the handler. It does not come back and finish the leftovers.

Third, Program continues normally printed. That is the entire point of handling: the crash was converted into a message, and the program survived.

Handling several exceptions differently. One try can be followed by many except clauses. Python tests them top to bottom and runs the first one whose class matches, then skips the others.

def percentage(total, count):
    try:
        total = int(total)
        result = total / count
        print("Percentage :", round(result, 2))
    except ValueError:
        print("ValueError  : marks must be a whole number")
    except ZeroDivisionError:
        print("ZeroDivisionError : subject count is zero")
    except TypeError as err:
        print("TypeError   :", err)

percentage("450", 5)
percentage("four fifty", 5)
percentage("450", 0)
percentage("450", "5")

Real output:

Percentage : 90.0
ValueError  : marks must be a whole number
ZeroDivisionError : subject count is zero
TypeError   : unsupported operand type(s) for /: 'int' and 'str'

Four calls, four different fates, one try block. The last clause uses as err, which binds the exception object to a name so you can print Python's own message instead of inventing your own. That is often the honest choice, because Python's message is more specific than anything you would write.

One handler for several classes. If two different failures deserve the same response, list the classes as a tuple in brackets:

data = ["12", "0", "xyz"]

for item in data:
    try:
        print(item, "->", 120 / int(item))
    except (ValueError, ZeroDivisionError) as err:
        print(item, "-> rejected:", type(err).__name__, "|", err)

Real output:

12 -> 10.0
0 -> rejected: ZeroDivisionError | division by zero
xyz -> rejected: ValueError | invalid literal for int() with base 10: 'xyz'

The brackets are compulsory. Writing except ValueError, ZeroDivisionError: without them is a SyntaxError: multiple exception types must be parenthesized. Also note type(err).__name__ — a neat way to print which class actually fired when one handler covers several.

Order matters, because exception classes form a family tree. Python takes the first clause that matches, not the most specific one. ZeroDivisionError is a child of ArithmeticError, so putting the parent first makes the child unreachable:

try:
    9 / 0
except ArithmeticError:
    print("caught by ArithmeticError (parent) - the child clause never gets a turn")
except ZeroDivisionError:
    print("this line is unreachable")

Real output:

caught by ArithmeticError (parent) - the child clause never gets a turn

The rule to remember: specific classes first, general classes last. You can see the family tree yourself. ZeroDivisionError.__mro__ hands back a tuple of the classes themselves, which prints as (, , , , ). To read it as plain names, run [c.__name__ for c in ZeroDivisionError.__mro__], which gives ['ZeroDivisionError', 'ArithmeticError', 'Exception', 'BaseException', 'object'].

One small trap with as: the name is deleted as soon as the except block ends. Running print(err) after the block gives NameError: name 'err' is not defined. If you need the message later, copy it into your own variable inside the handler.

Basic form try: risky except ExceptionClass: handler The rest of try is skipped from the point of failure onwards; execution resumes after the whole try statement.
Capture the object except ValueError as err: print(err) gives Python's message. err is deleted when the block ends, so copy it out if you need it later.
Several classes, one handler except (ValueError, ZeroDivisionError): Must be a bracketed tuple. Without brackets: SyntaxError: multiple exception types must be parenthesized.
Class name of the error type(err).__name__ Returns a string such as 'ZeroDivisionError'. Useful when one handler covers several classes.
Ordering rule child class first, parent class last Python runs the FIRST matching clause, not the most specific. ArithmeticError above ZeroDivisionError makes the latter unreachable.
Family tree check ZeroDivisionError.__mro__ Returns a tuple of class objects, not a list of strings. Observed order: ZeroDivisionError -> ArithmeticError -> Exception -> BaseException -> object. Use [c.__name__ for c in ZeroDivisionError.__mro__] to print plain names.
Remember
  • When a statement inside try raises, the rest of the try block is abandoned — Python jumps straight to the matching handler and never returns to finish it.
  • Multiple except clauses are tested top to bottom and only the first match runs, so write specific classes before general ones or the general one silently swallows everything.
  • To handle several classes the same way, use a bracketed tuple: except (ValueError, ZeroDivisionError):. Without brackets it is a SyntaxError.
  • except SomeError as err binds the exception object; print(err) gives Python's own message and type(err).__name__ gives the class name.
  • The name bound by as is deleted when the except block ends — using it afterwards raises NameError.

else and finally

Quick answer else holds the code that should run only when try succeeded, and finally holds cleanup that runs no matter what — even after a return and even while an unhandled exception is on its way out.

A full try statement can have four parts, and they always appear in this order: try, then except, then else, then finally. The syllabus names try, except and finally; else is a small bonus that makes programs tidier.

  • try — the risky code.
  • except — runs only if a matching exception happened.
  • else — runs only if the try block finished with no exception at all.
  • finally — runs always, success or failure, handled or not.

Here is all four, driven twice so you can see both paths:

def divide(a, b):
    print("CASE:", a, "/", b)
    try:
        print("  try     : starting")
        r = a / b
    except ZeroDivisionError:
        print("  except  : cannot divide by zero")
    else:
        print("  else    : no exception, answer =", r)
    finally:
        print("  finally : always runs")
    print()

divide(100, 4)
divide(100, 0)

Real output:

CASE: 100 / 4
  try     : starting
  else    : no exception, answer = 25.0
  finally : always runs

CASE: 100 / 0
  try     : starting
  except  : cannot divide by zero
  finally : always runs

Read the two blocks side by side. In the successful case else ran and except did not. In the failing case except ran and else did not. They are mutually exclusive. finally appeared in both.

Why bother with else at all? Because it keeps the try block small. Only the line that can actually raise belongs in try; everything that should follow a success goes in else. If you dump the follow-up code into try as well, an exception raised by the follow-up gets caught by a handler that was never meant for it, and you get a misleading error message.

finally beats return. This is the single most examined point in the chapter. Even if try or except executes a return, Python still runs finally before the function actually hands the value back:

def check(n):
    try:
        print("  in try, n =", n)
        return 100 // n
    except ZeroDivisionError:
        print("  in except, returning -1")
        return -1
    finally:
        print("  FINALLY ran anyway")

print("check(4)  gave", check(4))
print()
print("check(0)  gave", check(0))

Real output:

  in try, n = 4
  FINALLY ran anyway
check(4)  gave 25

  in try, n = 0
  in except, returning -1
  FINALLY ran anyway
check(0)  gave -1

Trace the first call. return 100 // n computed 25 — and then, before that 25 reached the caller, FINALLY ran anyway printed. The same happened after the return -1 inside except. A return cannot escape a finally.

finally even runs when nobody catches the exception. Watch the order here — the cleanup message appears before the traceback:

print("start")
try:
    print("opening resource")
    x = int("ninety")
except ZeroDivisionError:
    print("this handler does not match")
finally:
    print("finally: releasing resource")
print("this never prints")

Real output:

start
opening resource
finally: releasing resource
Traceback (most recent call last):
  File "resource.py", line 4, in 
    x = int("ninety")
ValueError: invalid literal for int() with base 10: 'ninety'

The only handler was for ZeroDivisionError, so the ValueError was not caught and the program did crash. But finally still got its turn on the way out, and the last print after the try statement never ran. That is precisely what finally is for: releasing something you grabbed, whether or not things went well.

The classic use: closing a file.

f = None
try:
    f = open("marks.txt", "r")
    for line in f:
        name, m = line.strip().split(",")
        print(name, "scored", int(m))
except ValueError as err:
    print("Bad record in file:", err)
finally:
    if f is not None:
        f.close()
    print("file closed?", f.closed)

With a file whose second line reads Diya,zero, the real output is:

Aarav scored 88
Bad record in file: invalid literal for int() with base 10: 'zero'
file closed? True

The file was closed even though the loop died half way. Notice the guard if f is not None — if open() itself had failed, f would never have been assigned and calling f.close() would raise a fresh error inside finally.

Which shapes are legal? These were checked by compiling each one:

ShapeLegal?What Python says
try + exceptYesthe normal form
try + finally, no exceptYescleanup happens, then the exception still propagates
try + except + else + finallyYesthe full form
try aloneNoSyntaxError: expected 'except' or 'finally' block
try + else, no exceptNoSyntaxError: expected 'except' or 'finally' block
finally written before exceptNoSyntaxError: invalid syntax
else written after finallyNoSyntaxError: invalid syntax

So else cannot stand on its own — it needs at least one except above it — but finally can pair with try with no handler at all.

Full statement order try: / except: / else: / finally: This order is compulsory. finally must be the last clause.
else clause else: runs only if try raised nothing Skipped whenever an except clause runs. Keeps the try block down to just the risky line.
finally clause finally: runs no matter what Runs after a return, and runs before the traceback prints when the exception is unhandled.
try with finally only try: ... finally: ... Legal with no except. The exception is not silenced — cleanup runs, then it propagates.
Safe file close finally: if f is not None: f.close() The guard matters: if open() failed, f was never bound and f.close() would raise inside finally.
Check a file is shut f.closed Returns True after close(). Observed as True even when the loop above died on a bad record.
Remember
  • Order is fixed: try, then except, then else, then finally. Writing finally before except, or else after finally, is a SyntaxError.
  • else and except are mutually exclusive — else runs only when try completed with no exception, so it never runs alongside a handler.
  • finally runs even when try or except executes a return: observed FINALLY ran anyway printing before the returned value reached the caller.
  • finally also runs when the exception is never caught — the cleanup message printed before the traceback, and the statement after the try block did not run.
  • try with only finally and no except is legal; try with only else is a SyntaxError.

Three Traps the Board Loves

Quick answer A bare except: swallows your own typos and Ctrl+C, an exception raised inside except is never caught by a sibling handler of the same try, and the bare clause must always be written last.

These three points separate a full-marks answer from a half-marks one. All three are shown running.

Trap 1: the bare except: lies to you. Written with no class after it, except: catches absolutely everything — including mistakes that have nothing to do with the error you were guarding against. Here the programmer typed totl instead of total:

total = 450
count = 5

print("--- version A: bare except ---")
try:
    average = totl / count          # typo: totl, not total
    print("Average:", average)
except:
    print("Please check the number of subjects.")

print()
print("--- version B: specific except ---")
try:
    average = totl / count          # same typo
    print("Average:", average)
except ZeroDivisionError:
    print("Please check the number of subjects.")

Real output:

--- version A: bare except ---
Please check the number of subjects.

--- version B: specific except ---
Traceback (most recent call last):
  File "average.py", line 14, in 
    average = totl / count          # same typo
              ^^^^
NameError: name 'totl' is not defined. Did you mean: 'total'?

Version A printed a confident, completely wrong diagnosis. There is nothing whatsoever wrong with the number of subjects — count is 5. The real problem is a spelling mistake, and the bare except: hid it. Version B, which names the class it actually expects, let the NameError through, and Python not only reported it but suggested the fix: Did you mean: 'total'? The bare except threw that help in the bin.

The bare except also eats Ctrl+C. When a user presses Ctrl+C, Python raises KeyboardInterrupt. A bare except: catches it, so the program refuses to stop:

print("A) bare except and Ctrl+C")
try:
    raise KeyboardInterrupt        # this is what Ctrl+C does
except:
    print("   swallowed! the user pressed Ctrl+C and the program ignored it")

print("B) except Exception and Ctrl+C")
try:
    try:
        raise KeyboardInterrupt
    except Exception:
        print("   this does NOT print")
except KeyboardInterrupt:
    print("   not swallowed - Ctrl+C passed straight through except Exception")

print("C) is KeyboardInterrupt an Exception?",
      issubclass(KeyboardInterrupt, Exception))
print("   is ZeroDivisionError an Exception?",
      issubclass(ZeroDivisionError, Exception))
print("   parents of ZeroDivisionError:",
      [c.__name__ for c in ZeroDivisionError.__mro__])

Real output:

A) bare except and Ctrl+C
   swallowed! the user pressed Ctrl+C and the program ignored it
B) except Exception and Ctrl+C
   not swallowed - Ctrl+C passed straight through except Exception
C) is KeyboardInterrupt an Exception? False
   is ZeroDivisionError an Exception? True
   parents of ZeroDivisionError: ['ZeroDivisionError', 'ArithmeticError', 'Exception', 'BaseException', 'object']

That last line explains everything. The root of the tree is BaseException. Exception sits below it and is the parent of all ordinary programming errors. KeyboardInterrupt and SystemExit deliberately sit outside Exception, directly under BaseException, precisely so that a sensible catch-all does not trap them. A bare except: is equivalent to except BaseException: and therefore traps them anyway. If you genuinely need a catch-all, write except Exception: and never a bare one.

Trap 2: an exception raised inside except is not caught by the same try. Once a handler starts running, the try statement has already made its choice. Its other except clauses are no longer candidates:

fees = {"Aarav": 25000}

try:
    print("due:", fees["Diya"])
except KeyError:
    print("handler running: not found, using default rate")
    print("default:", 25000 / 0)      # a SECOND exception, inside except
except ZeroDivisionError:
    print("does this catch it? NO - watch the output")
finally:
    print("finally still runs")
print("never reached")

Real output:

handler running: not found, using default rate
finally still runs
Traceback (most recent call last):
  File "fees.py", line 4, in 
    print("due:", fees["Diya"])
                  ~~~~^^^^^^^^
KeyError: 'Diya'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "fees.py", line 7, in 
    print("default:", 25000 / 0)      # a SECOND exception, inside except
                      ~~~~~~^~~
ZeroDivisionError: division by zero

The except ZeroDivisionError clause sitting right there, four lines away, did nothing. Its message never printed and the program crashed. Python even labels the situation for you: During handling of the above exception, another exception occurred. That sentence in a traceback always means the same thing — your handler itself broke.

Note also that finally still ran, on schedule, before the crash. And print("never reached") never ran, because the second exception was still travelling upwards.

The fix is a nested try. If the recovery code is itself risky, give it its own protection:

fees = {"Aarav": 25000}
installments = 0

try:
    print("due:", fees["Diya"])
except KeyError:
    print("handler running: name not in fee register")
    try:
        print("per installment:", 25000 / installments)
    except ZeroDivisionError:
        print("inner handler: installment count is zero, showing full amount 25000")
finally:
    print("finally runs")

print("program continues")

Real output:

handler running: name not in fee register
inner handler: installment count is zero, showing full amount 25000
finally runs
program continues

No crash, and program continues printed this time.

Trap 3: the bare clause must be written last. If you do use except:, Python forces it to the bottom. Writing it above a named clause does not merely make the named one unreachable — it is refused outright:

SyntaxError: default 'except:' must be last

Contrast that with the parent-class ordering problem from the previous section: writing except ArithmeticError: above except ZeroDivisionError: compiles fine and simply makes the second clause dead code. Python protects you from the bare-except ordering mistake, but not from the parent-class one. That asymmetry is worth remembering.

Bare except (avoid) except: Same as except BaseException. Catches NameError typos, KeyboardInterrupt and SystemExit. Almost never what you want.
Safe catch-all except Exception: Catches ordinary programming errors but lets Ctrl+C and sys.exit() pass through, because they derive from BaseException.
Prove it yourself issubclass(KeyboardInterrupt, Exception) Observed: False. For ZeroDivisionError the same call gives True.
Bare clause position except: must be the final clause Otherwise: SyntaxError: default 'except:' must be last.
Error inside a handler raised in except -> not caught by sibling except Traceback shows 'During handling of the above exception, another exception occurred'. finally still runs; code after the try does not.
Fix: nested try except KeyError: try: risky recovery except ZeroDivisionError: ... Give risky recovery code its own try. This is the only way to handle a failure inside a handler.
Remember
  • A bare except: catches everything, so a simple typo reports as a completely different, invented problem — the observed run blamed the subject count when the real fault was totl instead of total.
  • issubclass(KeyboardInterrupt, Exception) is False. KeyboardInterrupt and SystemExit sit under BaseException, so a bare except: swallows Ctrl+C while except Exception: correctly lets it through.
  • An exception raised inside an except block is NOT offered to the sibling except clauses of the same try — Python prints "During handling of the above exception, another exception occurred" and the program crashes.
  • finally still runs while that second exception is propagating, but any statement after the try block does not.
  • Writing a bare except: above a named clause is refused at compile time: SyntaxError: default 'except:' must be last. Writing a parent class above a child compiles fine and silently creates dead code.

Putting It to Work

Quick answer Three complete programs — a retry loop for user input, a marks-file reader that survives a missing file, and a UPI payment check that raises its own ValueError — showing where each clause earns its place.

Theory done. Here are three programs of the kind the board actually asks for.

1. Keep asking until the input is valid. int(input()) is the most common source of ValueError in student programs. Wrap it in a loop and the program stops being fragile:

def get_marks(subject):
    while True:
        raw = input("Marks in " + subject + ": ")
        print(raw)                 # echo what was typed, so the run is easy to read
        try:
            m = int(raw)
        except ValueError:
            print("   -> not a whole number, type it again")
        else:
            if 0 <= m <= 100:
                return m
            print("   -> marks must be between 0 and 100")

total = 0
for sub in ["Physics", "CS"]:
    total += get_marks(sub)

print("Total =", total, "/ 200")

A real run where the user typed ninety, then 150, then 88, then 95:

Marks in Physics: ninety
   -> not a whole number, type it again
Marks in Physics: 150
   -> marks must be between 0 and 100
Marks in Physics: 88
Marks in CS: 95
Total = 183 / 200

Study where the two rejections come from, because they are different in kind. ninety is a format problem — int() could not build a number, so except ValueError handled it. 150 is a rule problem — int() succeeded perfectly, 150 is a fine integer, it just is not a legal mark. That check belongs in else, not in try. Exception handling is for things that go wrong in the language; ordinary if statements are for things that go wrong in your rules. Do not use exceptions where an if will do.

2. Read a marks file that may not exist. This is the standard file-handling question, with three separate failures covered:

def show_report(filename):
    print("Reading:", filename)
    f = None
    try:
        f = open(filename, "r")
        total = 0
        for line in f:
            name, marks = line.strip().split(",")
            total += int(marks)
            print("  ", name, "->", marks)
    except FileNotFoundError:
        print("   File not found. Check the name and the folder.")
    except ValueError as err:
        print("   Corrupt record:", err)
    else:
        print("   Total =", total)
    finally:
        if f is not None:
            f.close()
        print("   done (file closed:", f.closed if f else "never opened", ")")
    print()

show_report("class12.txt")
show_report("class11.txt")

With class12.txt containing three comma-separated records and class11.txt not existing at all, the real output is:

Reading: class12.txt
   Aarav -> 88
   Diya -> 91
   Kabir -> 76
   Total = 255
   done (file closed: True )

Reading: class11.txt
   File not found. Check the name and the folder.
   done (file closed: never opened )

Everything from this chapter is visible in that output. else printed the total only on the successful run. finally printed on both runs. And the f is not None guard did real work on the second run — open() raised before f was ever assigned, so f was still None and f.close() was correctly skipped.

You may be wondering why with open(...) was not used, since it closes the file automatically. It does — a file opened with with is closed even when an exception is raised inside the block, which was checked and confirmed. But with does nothing about a file that could not be opened in the first place. So the honest answer is: use with for closing, and still wrap it in try/except FileNotFoundError for opening. The version above uses finally explicitly because the syllabus asks you to be able to write it that way.

3. Raising your own exception. Sometimes you are the one who knows the data is bad. raise lets you start an exception deliberately, and a normal except catches it:

balance = {"aarav@upi": 2500, "diya@upi": 180}

def pay(sender, amount):
    try:
        amt = int(amount)
        if amt <= 0:
            raise ValueError("amount must be positive")
        left = balance[sender] - amt
        if left < 0:
            raise ValueError("insufficient balance")
    except KeyError:
        print(sender, ": no such UPI id")
    except ValueError as err:
        print(sender, ": payment refused -", err)
    else:
        balance[sender] = left
        print(sender, ": paid Rs", amt, "| balance Rs", left)
    finally:
        print("   [transaction logged]")

pay("aarav@upi", "900")
pay("diya@upi", "900")
pay("kabir@upi", "100")
pay("aarav@upi", "1o0")

Real output:

aarav@upi : paid Rs 900 | balance Rs 1600
   [transaction logged]
diya@upi : payment refused - insufficient balance
   [transaction logged]
kabir@upi : no such UPI id
   [transaction logged]
aarav@upi : payment refused - invalid literal for int() with base 10: '1o0'
   [transaction logged]

Two details reward a close look. The fourth call passed "1o0" — the letter o instead of a zero, a genuine typing slip — and it was refused by the very same except ValueError clause that caught the deliberate raise ValueError("insufficient balance"). One handler, two sources: a raise you wrote and a failure inside int(). That is the whole point of using a standard class rather than inventing something.

Second, look at the else clause. The line balance[sender] = left is the one that actually moves the money, and it sits in else — so it can only run when every check passed. Had it been the last line of try, a later failure could have left the balance already changed. And [transaction logged] printed on all four calls, because a refused payment is still an event worth recording. That is what finally is for in real software.

Input retry pattern while True: try: n = int(input(...)) except ValueError: print('try again') else: return n The else clause is where the range check and the return belong, so try holds only the line that can raise.
Missing file except FileNotFoundError: Subclass of OSError. Fires from open() in read mode; open() in 'w' mode creates the file instead.
Raise deliberately raise ValueError('insufficient balance') Jumps to a matching except. The text becomes str(err), so print(err) shows your message.
Auto-closing file with open(fname) as fp: Confirmed to close the file even when an exception is raised inside the block. Still wrap it in try/except for FileNotFoundError.
Guarded close finally: if f is not None: f.close() Needed because a failed open() leaves f unassigned; the guard was actually exercised on the missing-file run.
Commit only on success else: balance[sender] = left Put the state-changing line in else so a later failure cannot leave data half-updated.
Remember
  • Use exceptions for what the language rejects (int("ninety")) and plain if statements for what your rules reject (marks above 100) — the observed run shows the two arriving through different clauses.
  • A while True loop with try/except ValueError/else return is the standard, reusable pattern for validating user input.
  • The f = None guard is not decoration: on the missing-file run, open() raised before f was assigned, so the if f is not None test correctly skipped f.close().
  • with open(...) closes the file even when an exception occurs inside the block, but it does nothing for a file that cannot be opened — you still need except FileNotFoundError.
  • raise ValueError("message") lets you start an exception yourself, and the same except ValueError clause catches both your raise and a genuine int() failure.

The formula sheet

Every formula in this chapter, in one place — screenshot it before your exam.

ExceptionClass: reason
Traceback last line
10 / 0
ZeroDivisionError
int('abc')
ValueError
'Rs ' + 500
TypeError
{'Aarav': 88}['Diya']
KeyError
open('fees.txt')
FileNotFoundError
try: risky except ExceptionClass: handler
Basic form
except ValueError as err:
Capture the object
except (ValueError, ZeroDivisionError):
Several classes, one handler
type(err).__name__
Class name of the error
child class first, parent class last
Ordering rule
ZeroDivisionError.__mro__
Family tree check
try: / except: / else: / finally:
Full statement order
else: runs only if try raised nothing
else clause
finally: runs no matter what
finally clause
try: ... finally: ...
try with finally only
finally: if f is not None: f.close()
Safe file close
f.closed
Check a file is shut
except:
Bare except (avoid)
except Exception:
Safe catch-all
issubclass(KeyboardInterrupt, Exception)
Prove it yourself
except: must be the final clause
Bare clause position
raised in except -> not caught by sibling except
Error inside a handler
except KeyError: try: risky recovery except ZeroDivisionError: ...
Fix: nested try
while True: try: n = int(input(...)) except ValueError: print('try again') else: return n
Input retry pattern
except FileNotFoundError:
Missing file
raise ValueError('insufficient balance')
Raise deliberately
with open(fname) as fp:
Auto-closing file
finally: if f is not None: f.close()
Guarded close
else: balance[sender] = left
Commit only on success

Test yourself

Tap an answer to check it instantly — you'll see why it's right, and what to revise if it isn't.

0 correct · 0/12 answered
Q1

What is the output?L = [10, 20, 30]try: print(L[3])except IndexError: print("A")except Exception: print("B")else: print("C")finally: print("D")

Q2

What is the output?def f(x): try: return 10 / x except ZeroDivisionError: return "inf" finally: print("bye", end=" ")print(f(2))print(f(0))

Q3

What is the output?d = {"a": 1, "b": 2}for k in ["a", "c", "b"]: try: print(d[k], end=" ") except KeyError: print("?", end=" ") finally: print("|", end=" ")

Q4

What is the output?try: x = int("12.5") print("got", x)except TypeError: print("TypeError")except ValueError: print("ValueError")finally: print("end")

Q5

What is the output?try: print("P", end=" ") n = 5 / 1except ZeroDivisionError: print("Q", end=" ")else: print("R", end=" ") print(int("7") + n, end=" ")finally: print("S", end=" ")print("T")

Q6

What is the output?S = "Priodemy"try: print(S[2], end=" ") print(S[20], end=" ")except IndexError: print("IE", end=" ")except KeyError: print("KE", end=" ")finally: print("F", end=" ")print("done")

Q7

What is the output?v = ["100", "0", "abc"]c = 0for x in v: try: r = 100 / int(x) except ValueError: c += 1 except ZeroDivisionError: c += 10 else: c += 100 finally: c += 1000print(c)

Q8

What is the output?try: print("A", end="") raise KeyError("x")except KeyError: print("B", end="") print(int("nine"), end="")except ValueError: print("C", end="")finally: print("D", end="")print("E")

Q9

Why is a bare except: considered dangerous even in a short program?

Q10

Which of the following is a SyntaxError in Python 3?

Q11

What happens when this is run?try: x = 1except: passexcept ValueError: pass

Q12

In a try statement, except ArithmeticError: is written above except ZeroDivisionError: and the try block executes 9 / 0. What happens?

NCERT solutions & previous-year questions

Step-by-step model answers — tap a question to reveal the full solution.

NCERT questions 6

1 What is an exception? How is an exception different from a syntax error?Introduction to exceptions

An exception is an error detected while the program is running. Python creates an object describing the problem, stops normal execution at that point, and looks for a handler. If no handler is found, the program terminates and a traceback is printed.

A syntax error is a violation of Python's grammar. It is detected while Python is translating the source file, before execution begins, so not even the first statement runs.

PointSyntax errorException
When detectedWhile translating, before the program startsWhile the program is running
CauseWrong grammar in the codeWrong data or conditions at runtime
Does earlier code run?No, nothing runsYes, everything before the failing statement runs
Can try-except help?No, the code must be correctedYes, this is exactly what try-except is for

Syntax error, executed. The file below has a valid print on line 1 and a missing colon on line 2:

print("Line 1 runs fine")
if marks > 40
    print("Pass")

Output:

  File "syntaxdemo.py", line 2
    if marks > 40
                 ^
SyntaxError: expected ':'

The words Line 1 runs fine never appeared, proving nothing was executed.

Exception, executed.

marks = 450
total = 0
print("Percentage:", marks / total)
print("This line never runs")

Output:

Traceback (most recent call last):
  File "marks.py", line 3, in 
    print("Percentage:", marks / total)
                         ~~~~~~^~~~~~~
ZeroDivisionError: division by zero

Here lines 1 and 2 executed normally. The failure came from the value 0 being stored in total, not from the way the code was written — the identical statement works perfectly if total is 5.

2 Name any four built-in exceptions in Python and state the situation in which each is raised.Built-in exception classes

Every message below was produced by actually running the code shown.

ExceptionRaised whenExample and message
ZeroDivisionErrorThe right operand of /, // or % is zero10 / 0 gives 'division by zero'
ValueErrorA function gets an argument of the right type but an unusable valueint("abc") gives "invalid literal for int() with base 10: 'abc'"
TypeErrorAn operation is applied to an object of an inappropriate type"Rs " + 500 gives 'can only concatenate str (not "int") to str'
NameErrorA name is used that has not been definedtotl_marks gives "name 'totl_marks' is not defined"
IndexErrorA list or string is indexed beyond its range[10, 20, 30][5] gives 'list index out of range'
KeyErrorA dictionary is accessed with a key that is not present{"Aarav": 88}["Diya"] gives 'Diya'
FileNotFoundErrorA file opened for reading does not existopen("fees.txt") gives "[Errno 2] No such file or directory: 'fees.txt'"
AttributeErrorAn attribute or method name does not exist on the object"88".upperr() gives "'str' object has no attribute 'upperr'"

The distinctions that are actually tested:

  • ValueError vs TypeError. int("abc") is a ValueError because int() is perfectly willing to accept a string — it just cannot make a number from those characters. "Rs " + 500 is a TypeError because the + operator cannot work with a str on one side and an int on the other at all.
  • IndexError vs KeyError. Positions in lists and strings give IndexError; missing dictionary keys give KeyError. Both are children of LookupError, confirmed by issubclass(KeyError, LookupError) returning True.
  • FileNotFoundError is a subclass of OSError, and IOError is OSError evaluates to True — IOError is only an older name for the same class.
  • ZeroDivisionError wording. 10 / 0 says 'division by zero', but 10 // 0 says 'integer division or modulo by zero' and 10 % 0 says 'integer modulo by zero'. Same class, three different messages.
3 When is a ZeroDivisionError raised? Write a program that accepts two numbers from the user and displays their quotient, handling the exception if the second number is zero.try-except in practice

A ZeroDivisionError is raised whenever the right-hand operand of /, // or % is zero. Division by zero has no defined result in mathematics, so Python refuses rather than inventing an answer.

Program (the print after each input simply echoes what was typed so the sample runs below read clearly):

num = input("Enter numerator: ");   print(num)
den = input("Enter denominator: "); print(den)

try:
    q = int(num) / int(den)
except ValueError:
    print("Please enter integers only.")
except ZeroDivisionError:
    print("Denominator cannot be zero.")
else:
    print("Quotient =", q)
finally:
    print("Thank you.")

Run 1 — valid input:

Enter numerator: 250
Enter denominator: 5
Quotient = 50.0
Thank you.

Run 2 — denominator zero:

Enter numerator: 250
Enter denominator: 0
Denominator cannot be zero.
Thank you.

Run 3 — non-numeric input:

Enter numerator: 250
Enter denominator: five
Please enter integers only.
Thank you.

Points worth marks: a second handler for ValueError is included because int() is just as likely to fail as the division — a user typing five is at least as common as one typing 0. The successful result is printed from else, not from inside try, so that only the risky statement sits in the protected block. And Thank you. comes from finally, which is why it appears in all three runs. Note also that the quotient is 50.0 and not 50, because / always produces a float.

4 What is the use of the finally clause? Explain with an example.The finally clause

The finally clause holds code that must run whatever happens — whether the try block succeeded, whether an exception was raised and handled, whether it was raised and not handled, and even if the function executed a return. It is used for cleanup: closing files, releasing connections, writing a log entry.

Example 1 — finally runs even after a return.

def check(n):
    try:
        print("  in try, n =", n)
        return 100 // n
    except ZeroDivisionError:
        print("  in except, returning -1")
        return -1
    finally:
        print("  FINALLY ran anyway")

print("check(4)  gave", check(4))
print()
print("check(0)  gave", check(0))

Output:

  in try, n = 4
  FINALLY ran anyway
check(4)  gave 25

  in try, n = 0
  in except, returning -1
  FINALLY ran anyway
check(0)  gave -1

In both calls the value was computed by a return, yet FINALLY ran anyway printed before the value reached the caller. A return cannot skip a finally.

Example 2 — finally runs even when the exception is not caught.

print("start")
try:
    print("opening resource")
    x = int("ninety")
except ZeroDivisionError:
    print("this handler does not match")
finally:
    print("finally: releasing resource")
print("this never prints")

Output:

start
opening resource
finally: releasing resource
Traceback (most recent call last):
  File "resource.py", line 4, in 
    x = int("ninety")
ValueError: invalid literal for int() with base 10: 'ninety'

The only handler was for a different class, so the program did crash — but the cleanup line still printed, and it printed before the traceback. Meanwhile the statement written after the whole try block never ran.

Typical use — closing a file safely:

finally:
    if f is not None:
        f.close()

The if f is not None guard is necessary because if open() itself raised, f was never assigned, and calling f.close() would raise a fresh error inside the cleanup block.

5 Explain the role of the else clause in a try statement. How is it different from writing the same code at the end of the try block?The else clause

The else clause of a try statement runs only if the try block finished without raising any exception. If any except clause runs, else is skipped. The two are mutually exclusive.

Both paths, executed:

def divide(a, b):
    print("CASE:", a, "/", b)
    try:
        print("  try     : starting")
        r = a / b
    except ZeroDivisionError:
        print("  except  : cannot divide by zero")
    else:
        print("  else    : no exception, answer =", r)
    finally:
        print("  finally : always runs")
    print()

divide(100, 4)
divide(100, 0)

Output:

CASE: 100 / 4
  try     : starting
  else    : no exception, answer = 25.0
  finally : always runs

CASE: 100 / 0
  try     : starting
  except  : cannot divide by zero
  finally : always runs

In the successful case else ran and except did not; in the failing case the reverse. finally ran in both.

Why not just put that code at the end of try? Because then an exception raised by the follow-up code would be caught by a handler written for something else, producing a misleading message. Compare:

try:
    f = open("data.txt")
    value = int(f.readline())
except FileNotFoundError:
    print("file missing")

If the file exists but its first line reads abc, int() raises ValueError. That is not caught here, so the program crashes — but worse, if the handler had been a broad one it would have reported a missing file when the file was present and readable. Moving the follow-up into else keeps try down to only the statement that the handler is actually about:

try:
    f = open("data.txt")
except FileNotFoundError:
    print("file missing")
else:
    value = int(f.readline())
    f.close()

Be clear about what this does and does not fix. Both versions were run against a data.txt whose first line is abc, and both still crash with ValueError: invalid literal for int() with base 10, because neither one handles a ValueError. What changes is the diagnosis. With the conversion inside try, a broader handler would have caught the ValueError and reported file missing about a file that opened perfectly. With the conversion in else, the except FileNotFoundError clause guards only open(), which is the single statement it was written for.

One rule to remember: else cannot be used without at least one except above it. Compiling a try block with only an else clause gives SyntaxError: expected 'except' or 'finally' block.

6 Write a program that reads a text file and displays its contents, showing a suitable message if the file does not exist. Use try, except, else and finally.File handling with exceptions

Program:

def read_file(fname):
    try:
        f = open(fname, "r")
    except FileNotFoundError:
        print(fname, ": file does not exist")
    else:
        print(fname, ":", f.read().strip())
        f.close()
    finally:
        print("attempt over")

with open("notes.txt", "w") as fp:
    fp.write("Priodemy Class 12 CS")

read_file("notes.txt")
read_file("missing.txt")

Output:

notes.txt : Priodemy Class 12 CS
attempt over
missing.txt : file does not exist
attempt over

How the four clauses are used here:

  • try contains only open() — the one statement that can raise FileNotFoundError. Nothing else is put inside it.
  • except FileNotFoundError gives a readable message instead of a traceback.
  • else holds the reading and closing, which can only make sense if the file actually opened. Note that f is safe to use here precisely because else runs only on success.
  • finally prints on both runs, showing it is independent of the outcome.

A common mistake to avoid. Do not write the reading inside try like this:

try:
    f = open(fname, "r")
    print(f.read())
    f.close()
except FileNotFoundError:
    print("file does not exist")

It looks equivalent but has a real bug: if an exception occurs while reading, f.close() is skipped and the file is left open. Either use else as above with the close inside it, or move the close into finally guarded by if f is not None.

Note on with. Writing with open(fname) as f: closes the file automatically even if an exception occurs inside the block — this was verified, with f.closed reporting True after a ValueError was raised mid-block. But with does nothing when the file cannot be opened at all, so the except FileNotFoundError is still required.

Previous-year board questions 4

Q1 Predict the output of the following code. (3 marks)def calc(a, b): try: c = a / b except ZeroDivisionError: print("Zero", end="#") c = 0 except TypeError: print("Type", end="#") c = -1 finally: print("Done", end="#") return cprint(calc(10, 2))print(calc(10, 0))print(calc(10, "2")) Board pattern (3 marks)

Output:

Done#5.0
Zero#Done#0
Type#Done#-1

Working, call by call.

calc(10, 2)10 / 2 succeeds and stores 5.0 in c. No handler runs. finally prints Done#. The function returns 5.0, and the outer print displays it on the same line, giving Done#5.0. Note the value is 5.0 and not 5, because the / operator always produces a float.

calc(10, 0)10 / 0 raises ZeroDivisionError. The first handler matches, printing Zero# and setting c to 0. finally then prints Done#. Returns 0, giving Zero#Done#0.

calc(10, "2") — dividing an int by a str raises TypeError (the full message is unsupported operand type(s) for /: 'int' and 'str'). The second handler matches, printing Type# and setting c to -1. finally prints Done#. Returns -1, giving Type#Done#-1.

Marks are usually lost on three things here: writing 5 instead of 5.0 for the first call; forgetting that finally runs on the successful path too, so Done# appears on all three lines; and misreading end="#", which suppresses the newline so the returned value lands on the same line as the messages.

Q2 Differentiate between a syntax error and an exception, giving one example of each. Also state whether each can be handled using a try-except block. (2 marks) Board pattern (2 marks)

Difference. A syntax error is a violation of Python's grammar, found while the source file is being translated, so the program never begins to run. An exception is a runtime error, raised while the program is executing, caused by the actual data or conditions encountered.

Example of a syntax error:

print("Line 1 runs fine")
if marks > 40
    print("Pass")
  File "syntaxdemo.py", line 2
    if marks > 40
                 ^
SyntaxError: expected ':'

The colon is missing after the condition. Observe that Line 1 runs fine was not printed, confirming that no statement executed.

Example of an exception:

marks = 450
total = 0
print("Percentage:", marks / total)
Traceback (most recent call last):
  File "marks.py", line 3, in 
    print("Percentage:", marks / total)
                         ~~~~~~^~~~~~~
ZeroDivisionError: division by zero

Lines 1 and 2 executed normally; the failure came from the value stored in total.

Can they be handled? An exception can be handled with try-except — that is its purpose. A syntax error cannot, because the try-except block itself is part of the same file and is never executed; the code has to be corrected instead.

Q3 Write a function count_a_lines(fname) that reads a text file and displays all lines beginning with the letter 'A', returning the count of such lines. If the file does not exist, display a suitable message and return 0. The function must print a message when the read attempt finishes, whether or not it succeeded. (3 marks) Board pattern (3 marks)

Program:

def count_a_lines(fname):
    try:
        f = open(fname, "r")
    except FileNotFoundError:
        print("Error: file", fname, "not found")
        return 0
    else:
        n = 0
        for line in f:
            if line.startswith("A"):
                print(line.strip())
                n += 1
        f.close()
        return n
    finally:
        print("-- read attempt finished --")

Driver and output. With STUDENT.TXT containing the four records Aarav Sharma 88, Diya Nair 91, Ananya Rao 79, Kabir Singh 76, and TEACHER.TXT not existing:

print("Count =", count_a_lines("STUDENT.TXT"))
print()
print("Count =", count_a_lines("TEACHER.TXT"))
Aarav Sharma 88
Ananya Rao 79
-- read attempt finished --
Count = 2

Error: file TEACHER.TXT not found
-- read attempt finished --
Count = 0

The examinable point in this question is the interaction between return and finally. Both the except branch and the else branch return a value, yet -- read attempt finished -- printed on both runs, and in each case it printed before the returned value was displayed by the caller. That is the guarantee finally gives: a return statement computes its value, then the finally block runs, and only then does control leave the function.

Design notes. Only open() sits inside try, because that is the single statement FileNotFoundError can come from. The reading loop is in else, where f is guaranteed to be a valid open file. The file is closed inside else, just before the return, because that is the only branch in which a file object exists at all — on the missing-file run there is nothing to close, which is why finally carries only the message. line.strip() removes the trailing newline so the output is not double-spaced, and startswith("A") is case-sensitive — use line.upper().startswith("A") if lower-case 'a' should also count.

Q4 Study the code below and answer the questions that follow. (1 + 1 + 1 = 3 marks)fees = {"Aarav": 25000}try: print("due:", fees["Diya"])except KeyError: print("handler running: not found, using default rate") print("default:", 25000 / 0)except ZeroDivisionError: print("caught the division error")finally: print("finally still runs")print("never reached")(a) Name the exception raised by the first statement inside try.(b) Will the except ZeroDivisionError clause handle the error raised inside the KeyError handler? Justify.(c) Write the complete output. Board pattern (3 marks)

(a) fees["Diya"] looks up a key that is not present in the dictionary, so a KeyError is raised. Its message is simply the missing key, 'Diya'. (A missing dictionary key gives KeyError, not IndexError — IndexError is for out-of-range list and string positions.)

(b) No, it will not. Once a try statement has selected and entered a handler, that try statement is finished choosing — its remaining except clauses are no longer candidates. The ZeroDivisionError raised by 25000 / 0 occurs inside the KeyError handler, so it is passed outward to any enclosing try statement, not sideways to a sibling clause of the same try. Since there is no enclosing try here, the program terminates. Python signals this situation explicitly with the line During handling of the above exception, another exception occurred.

(c) Output:

handler running: not found, using default rate
finally still runs
Traceback (most recent call last):
  File "fees.py", line 3, in 
    print("due:", fees["Diya"])
                  ~~~~^^^^^^^^
KeyError: 'Diya'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "fees.py", line 6, in 
    print("default:", 25000 / 0)
                      ~~~~~~^~~
ZeroDivisionError: division by zero

(The line numbers 3 and 6 are those of the two failing statements in the code exactly as printed in the question.) Note two things in that output. caught the division error never printed, confirming the answer to (b). And finally still runs did print — a finally block executes even while a new, unhandled exception is on its way out — whereas print("never reached"), which sits after the whole try statement, did not run.

The correct fix is to give the risky recovery code its own protection with a nested try:

except KeyError:
    print("handler running: name not in fee register")
    try:
        print("per installment:", 25000 / installments)
    except ZeroDivisionError:
        print("inner handler: installment count is zero, showing full amount 25000")

With installments = 0, that version produces no crash at all:

handler running: name not in fee register
inner handler: installment count is zero, showing full amount 25000
finally runs
program continues

Part of Priodemy for School

Interactive CBSE lessons, Class 8–12 — free with every school on Priodemy EduSuite. Explore more chapters and labs on the Priodemy for School hub.

Ask AI