Press m or Esc to close
Your programs already crash. This chapter is about deciding what happens when they do — instead of letting Python decide.
You met ValueError, TypeError and
KeyError as accidents — red boxes that ended the program.
You learned to read a traceback and name the exception.
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.
Those same errors, but caught. A crash becomes a message, a retry, or a sensible default — and the program keeps running.
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.
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.
Blue = what you wrote · Green = what it printed · Red = what went wrong. In this chapter red boxes are the subject, not the failure.
main.py1marks = 50 2if marks > 40 3 print("Pass")
Syntax error — a breach of the grammar of the language, detected by the interpreter before the program runs. Also called a parsing error.
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.
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.
main.py1print("Report starting") 2marks = [90, 85, 70] 3n = 0 4print("Average:", sum(marks) / n) 5print("Report done")
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.
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.
Everything from §3 onwards exists to stop that red box appearing and print
Report done anyway.
average.pymarks = [90, 85, 70] average = sum(marks) / len(marks) * 100 print("Average:", average)
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.
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.
| Syntax error | Exception | Logical error | |
|---|---|---|---|
| Detected | before running | while running | never, by Python |
| Does any code run? | No | Yes, until the bad line | Yes, all of it |
| Python complains? | Yes | Yes | No |
Can try…except catch it? | No | Yes | No |
| Example | missing : | 50 / 0 | * 100 too many |
| You fix it by | correcting the grammar | writing a handler | testing the output |
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.
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.
try: import brokenexcept 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.
Classify each as a syntax error, an exception, or a logical error.
print("Total is " + 500)for i in range(5) (no colon)L = [1, 2, 3] then print(L[3])while i < 10 that never changes i| # | Kind | Name / reason |
|---|---|---|
| 1 | Exception | TypeError — can only concatenate str to str |
| 2 | Syntax error | missing colon; nothing runs |
| 3 | Logical error | runs fine, answer is wrong |
| 4 | Exception | IndexError — list index out of range |
| 5 | Logical error | an 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.
main.py1a = 10 2b = 0 3print(a / b)
main.py1n = int("12a")
main.py1print("Total: " + 500)
main.py1d = {"a": 1} 2print(d["b"])
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.
main.py1marks = 90 2print(total)
A name that was never bound — usually a typo.
main.py1L = [10, 20, 30] 2print(L[3])
Valid indices are 0, 1, 2 — never len(L).
main.py1f = open("marks.txt", "r")
The whole reason Chapter 4 needs this chapter.
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.
| Exception | Raised when… | Typical trigger |
|---|---|---|
SyntaxError | the grammar of Python is broken | missing :, unbalanced bracket |
IndentationError | indentation is wrong or inconsistent | a stray space at the start of a line |
NameError | a name is used before it is bound | misspelling a variable |
TypeError | an operation gets the wrong type | "a" + 5, len(5) |
ValueError | right type, unusable value | int("abc") |
ZeroDivisionError | the divisor is zero | a / 0, a % 0 |
IndexError | a sequence index is out of range | L[len(L)] |
KeyError | a dictionary key does not exist | d["missing"] |
IOError / FileNotFoundError | a file cannot be opened | open("nofile.txt") |
ImportError | a module cannot be found | import maths |
EOFError | input() hits end of input | Ctrl+D at a prompt |
OverflowError | a numeric result is too large | math.exp(1000) |
KeyboardInterrupt | the user interrupts the program | Ctrl+C |
TypeError or ValueError?The one distinction the paper tests, every year.
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
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
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.
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
| Line | Exception | Why |
|---|---|---|
| 1 | ZeroDivisionError | the bracket evaluates to 0 first |
| 2 | ValueError | a str is acceptable; this one is not convertible |
| 3 | IndexError | valid indices are 0, 1, 2 — tuples index like lists |
| 4 | TypeError | no integer has a length |
| 5 | KeyError | the key is absent |
| 6 | NameError | the 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.
no guard — main.pyprint("Start") a = 50 b = 0 print(a / b) print("End")
guardedprint("Start") a = 50 b = 0 try: print(a / b) except ZeroDivisionError: print("Cannot divide by zero") print("End")
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.
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.
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.
syntaxtry: # statements that might fail # — the SUSPECT code except ExceptionName: # what to do if that # exception is raised
except lines up with try, not inside it. Getting
that wrong is an IndentationError before you even test the logic.
Holds the suspect statements — the ones you believe might raise. Keep it short: only the lines that can actually fail.
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.
Every try must be followed by at least one except
or a finally. It cannot stand alone.
try block is abandonedskip.pytry: print("A") x = 10 / 0 print("B") except ZeroDivisionError: print("C") print("D")
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.
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.
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")
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.
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")
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.
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"))
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.
10.0 and not 10
Chapter 1, §3.1 — / always returns a float. Exception handling changed
nothing about that.
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)
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.
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.
exceptbare.pyvalues = ["8", "0", "abc"] for v in values: try: print(100 / int(v)) except ZeroDivisionError: print("Denominator is zero") except: print("Some other error")
"abc"? 🤔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")
Python refuses outright — a bare except before a named
one would make the named one unreachable.
except is dangerousA 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")
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")
The bug surfaces immediately, because the handler did not match.
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.
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"))
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.
Add one line so that all three calls print something sensible, without hiding real bugs.
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.
else — the block that runs when nothing went wrongwithelse.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)
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.
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.
finally — the block that always runswithfinally.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)
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.
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.
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
try → excepts → else →
finally. Putting else after finally, or
a bare except before a named one, is a
SyntaxError.
| Block | Optional? | Runs when |
|---|---|---|
try | required | always entered |
except | see note | its exception was raised |
else | optional | no exception was raised |
finally | optional | always, last |
A try needs at least one except or a
finally. else cannot appear without at least one
except.
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.
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")
ZeroDivisionError is handled. What happens on the amber line? 🤔① 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.
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.
| Scenario | try statements |
matching except |
else |
finally |
Program after |
|---|---|---|---|---|---|
| No exception | all run | skipped | runs | runs | continues |
| Exception, handled | stops at the failure | runs | skipped | runs | continues |
| Exception, not handled | stops at the failure | none matches | skipped | runs | terminates with a traceback |
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.
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")
① 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.
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))
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.
If finally contained return 99 instead of a
print, what would f(2) give?
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.
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")
raise — forcibly throws an exception.
Syntax: raise ExceptionName("message")
The message is optional and is shown after the colon in the traceback.
① "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.
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)
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.
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.
assert — a check you can state in one linemain.py1def negativecheck(number): 2 assert number >= 0, "OOPS... Negative Number" 3 print(number * number) 4 5negativecheck(100) 6negativecheck(-350)
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
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".
raise or assert?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")
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!"
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.
raise throws any exception you name; assert tests a
condition and throws AssertionError if it is false.
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")
raise inside an except do? 🤔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.
“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.”
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")
| Line | What happened |
|---|---|
print(L[1]) | printed 2 |
print(L[5]) | raised IndexError — try block abandoned |
print(L[2]) | never reached |
first except | matched, so the bare one was skipped |
finally | ran, as it always does |
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")
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.
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.
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")
① 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"
finally block? How is it different from
else?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.
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.
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))
"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.
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.
① 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.