Suppose three students in your class each write the same five-line program for the school computer lab: it asks for marks obtained and total marks, then prints the percentage. Two of them test it with sensible numbers like 450 and 500, get 90.0%, and submit it. The third student's little sister runs the same program and types the total marks as 0, because she is curious what happens. The program does not print a friendly message. It stops dead, throws a wall of red text at her, and the whole session is gone. That wall of red text is called a traceback, and the difference between a program that survives a bad input and one that collapses is exactly what this chapter is about: error handling.
This is not a cosmetic topic. Every UPI app, every IRCTC booking page, every school result portal is bombarded with unexpected input all day — wrong PINs, expired sessions, missing files, network drops. None of those systems are crash-proof by accident. They are built by programmers who assumed things would go wrong and wrote code that plans for it. In Python, the tool for that planning is the try/except block, and by the end of this chapter you will be able to read, trace, and write it correctly — which is also a very common CBSE Computer Science exam question type: "predict the output" for a program containing try/except.
Three kinds of "wrong" — and only one of them is what this chapter fixes
Before we can handle errors, we have to be precise about what kind of error we are even talking about, because Class 8 students often lump all three together and that confusion causes real marks loss in exams.
Syntax errors happen when the code you typed does not follow Python's grammar rules at all — a missing colon, a mismatched bracket, a misspelled keyword. Python refuses to even start running the program. For example, writing if marks > 90 without the trailing colon is a syntax error. There is no "handling" a syntax error at runtime — you simply have to fix the code before it can run.
Logical errors happen when the program runs perfectly, start to finish, without any complaint — and still gives the wrong answer. If you wrote percentage = marks + total / 100 instead of percentage = marks / total * 100, Python will happily compute a nonsense number and print it with total confidence. No error message appears, because as far as Python is concerned, nothing went wrong. Logical errors are caught by testing and careful tracing, not by error handling.
Runtime errors, also called exceptions, are the ones this chapter is about. The code is grammatically correct, and the logic is fine in general — but at one specific moment, while the program is running, something happens that Python cannot recover from on its own: dividing by zero, asking for the sixth item in a five-item list, trying to open a file that was never created. Python's response is to immediately stop executing the current block and raise an exception object that describes exactly what went wrong. If nothing catches that exception, the whole program terminates and Python prints the traceback you saw above.
The crucial insight is that exceptions are not bugs in the traditional sense — the code is correct, but the world handed it a situation it was not told how to deal with. Error handling is the discipline of telling Python, in advance, what to do when that situation shows up, instead of letting the whole program die.
The IRCTC mindset: always have a plan B
Here is an analogy that maps onto try/except almost exactly. Suppose you book a train ticket on IRCTC for a family trip. Your primary plan is: board the train, reach the destination. But a careful traveller also keeps a backup plan in mind: if the train is cancelled, what then? Maybe you have saved the number of a bus operator, or you know the next train's timing. You do not build the backup plan into your main journey — you only use it if the primary plan actually fails, and if the train does leave on time, you never think about the bus operator at all.
A try block is your primary plan: the code you actually want to run. An except block is your backup plan: code that runs only if something in the primary plan fails, and only for the specific kind of failure you named. If the train departs fine, the bus-operator plan is never touched. If the primary code runs fine, the except block is never touched either. That is the entire mental model — everything else in this chapter is detail on top of it.
Watching a program crash, then fixing it
Here is the percentage calculator exactly as a Class 8 student might first write it, with no error handling at all.
marks = int(input("Enter marks obtained: "))
total = int(input("Enter total marks: "))
percentage = (marks / total) * 100
print(f"Percentage: {percentage}%")
If the total is entered as 0, Python executes the third line, tries to compute marks / 0, and immediately raises ZeroDivisionError: division by zero. The fourth line never runs, and the program ends. If instead someone types a word like "eighty" where a number is expected, the very first line fails with ValueError: invalid literal for int() with base 10: 'eighty', because int() has no idea how to convert that text into a whole number.
Now here is the same program with the primary plan wrapped in a try block and two backup plans attached as except blocks, one per kind of failure we anticipate.
try:
marks = int(input("Enter marks obtained: "))
total = int(input("Enter total marks: "))
percentage = (marks / total) * 100
except ZeroDivisionError:
print("Total marks cannot be zero.")
except ValueError:
print("Please enter whole numbers only.")
else:
print(f"Percentage: {percentage}%")
finally:
print("Thank you for using the marks calculator.")
Let us trace this line by line for two different runs, because tracing is exactly what a CBSE exam question will ask you to do.
Run 1 — marks = 80, total = 0. Python enters the try block. Both int() conversions succeed, so marks becomes 80 and total becomes 0. The third line attempts 80 / 0, which raises ZeroDivisionError at that exact instant — the assignment to percentage never completes. Python immediately stops running the rest of the try block and looks for a matching except. The first one, except ZeroDivisionError, matches, so it runs and prints Total marks cannot be zero. Because an exception did occur, the else block is skipped entirely — it is reserved for the case where nothing went wrong. Finally, the finally block always runs regardless of what happened above, printing Thank you for using the marks calculator. The full output is two lines: Total marks cannot be zero. followed by Thank you for using the marks calculator.
Run 2 — marks = 450, total = 500. Both conversions succeed, and this time 450 / 500 * 100 evaluates cleanly to 90.0 with no exception raised anywhere in the try block. Because nothing went wrong, both except blocks are skipped, and control passes to the else block, which prints Percentage: 90.0%. The finally block then runs exactly as before, printing its thank-you line. Output: Percentage: 90.0% followed by Thank you for using the marks calculator.
Notice precisely what each clause is for. Code inside try is the code that might fail. Code inside except only runs if a matching failure actually happened. Code inside else only runs if no failure happened in the try block — it exists so that success-only code is visibly separated from the risky code, rather than being crammed inside the try block where it does not belong. Code inside finally runs every single time, success or failure, which makes it the right place for cleanup work such as closing a file or printing a closing message that must happen no matter what.
The full control flow, as a diagram
The flowchart below shows every path this structure can take. Follow the "error occurs" path with your finger for Run 1 above, then the "no error" path for Run 2, and you will see both traces land exactly where the printed output says they should.
Naming the right exception: a field guide
An except block only catches the exception type you name after it. To write useful backup plans, you need to recognise which situation produces which exception. Here are the six you will meet constantly in Class 8 and 9 Python programs, each with the exact code that triggers it.
- ZeroDivisionError — raised by any division or modulo with a zero divisor.
10 / 0raisesZeroDivisionError: division by zero. - ValueError — raised when a function receives an argument of the right type but an inappropriate value.
int("eighty")raisesValueError: invalid literal for int() with base 10: 'eighty'— the argument is a string, whichint()accepts in general, but this particular string cannot be parsed as a number. - TypeError — raised when an operation is applied to a value of the wrong type entirely.
"5" + 5raisesTypeError: can only concatenate str (not "int") to str, because Python will not silently guess whether you meant text-joining or arithmetic. - IndexError — raised when you ask a list, string, or tuple for a position that does not exist. If
marks_list = [78, 85, 92]and you writemarks_list[5], Python raisesIndexError: list index out of range, since valid positions here are only 0, 1, and 2. - KeyError — the dictionary equivalent of
IndexError. Ifstudent = {"name": "Aditi"}and you writestudent["marks"], Python raisesKeyError: 'marks'because that key was never stored. - FileNotFoundError — raised by
open()when the file path given does not exist on disk.open("results.txt")on a machine with no such file raisesFileNotFoundError: [Errno 2] No such file or directory: 'results.txt'.
Each of these is a distinct class, and Python decides which except block matches by checking the exception's class against the type you named, exactly the way it checks a value's type anywhere else. This is why you can write more than one except block after a single try — each one is a separate backup plan for a separate kind of failure, and Python tries them in the order you wrote them until one matches.
Misconception 1: order does not matter — it absolutely does
A mistake that costs marks in exams is writing a general except block before a specific one, without realising that every built-in exception, including ValueError, ZeroDivisionError, and the rest, is a more specific case of the built-in class Exception. Consider this code:
try:
value = int("abc")
except Exception:
print("Something went wrong")
except ValueError:
print("Invalid number")
Running int("abc") does raise a ValueError. But Python checks except clauses from top to bottom and stops at the first match. Since ValueError is a kind of Exception, the first clause, except Exception, matches immediately, and it is the one that runs — printing the vague Something went wrong. The second clause, except ValueError, is never reached; it is dead code, even though nothing about its syntax is wrong. The fix is a rule you should memorise: always list the most specific exception types first, and put any general except Exception last, as a genuine catch-all for cases you did not think to name individually.
Misconception 2: a bare except: is not a shortcut, it is a trap
Some students, trying to "handle everything at once," write a bare except: with no type named at all, planning to catch anything that could possibly go wrong. This is dangerous for two reasons. First, it catches every exception indiscriminately, including ones caused by a genuine bug elsewhere in your code — a misspelled variable name, for instance, raises NameError, and a bare except: will silently swallow that too, hiding a real mistake instead of surfacing it. Second, it even intercepts signals like the user pressing Ctrl+C to stop the program, which is almost never what you want. The professional habit — one CBSE examiners reward — is to name exactly the exceptions you expect, or at worst use except Exception (which still lets truly unusual system-level signals through), and never a bare except:.
Catching more than one type with a single block
When two different exception types deserve the identical response, you do not need to repeat yourself — Python lets you group types in a tuple after a single except:
try:
age = int(input("Enter your age: "))
fee = 500 / age
except (ValueError, ZeroDivisionError):
print("Enter a valid non-zero age.")
Here, whether the input cannot be converted to an integer at all (ValueError) or converts fine but is 0 (ZeroDivisionError), the single block handles both, because (ValueError, ZeroDivisionError) is checked as a set of acceptable matches, not as two separate clauses.
Raising your own exceptions
So far, exceptions have been things Python raises on its own when an operation genuinely cannot be completed. But you can also raise an exception deliberately, using the raise keyword, when your own code detects that something is invalid — even though Python itself would have been perfectly happy to continue. This matters because Python has no idea, for example, that a UPI PIN must be exactly four digits; that rule comes from you.
def set_upi_pin(pin):
if len(pin) != 4 or not pin.isdigit():
raise ValueError("UPI PIN must be exactly 4 digits.")
return "PIN set successfully."
try:
print(set_upi_pin("12a4"))
except ValueError as e:
print(f"Error: {e}")
Trace this carefully. Inside set_upi_pin, pin is the string "12a4", so len(pin) is 4 — the first half of the condition, len(pin) != 4, is False. But pin.isdigit() checks whether every character is a digit, and 'a' is not, so pin.isdigit() is False, which makes not pin.isdigit() True. Since the condition is "either half true," the whole if is True, and Python executes raise ValueError("UPI PIN must be exactly 4 digits."). This immediately exits the function — the return line is never reached — and the exception travels up to the try block that called the function, where except ValueError as e catches it. The as e part gives you a name for the exception object, and str(e) (which f"{e}" uses automatically) is the message text you passed to raise. So the final printed line is exactly Error: UPI PIN must be exactly 4 digits.
You are not limited to Python's built-in exception names either. Because exceptions are just classes, you can define your own by inheriting from Exception, which is useful when a generic name like ValueError does not describe your specific rule clearly enough for someone reading the code later:
class InsufficientMarksError(Exception):
pass
def check_pass(marks):
if marks < 33:
raise InsufficientMarksError(f"{marks} is below the passing mark of 33.")
return "Pass"
try:
print(check_pass(28))
except InsufficientMarksError as e:
print(f"Cannot promote: {e}")
Tracing this: check_pass(28) checks 28 < 33, which is True, so it raises InsufficientMarksError carrying the message "28 is below the passing mark of 33.". The calling try block's except InsufficientMarksError as e matches this exact custom class, and prints Cannot promote: 28 is below the passing mark of 33. The word pass inside the class definition simply means "this class needs no extra code of its own — it only needs to exist as a distinct, nameable type."
Predict the output
Trace each of these the way we traced the marks calculator above, line by line, before checking the explanation that follows it.
data = {"name": "Rohan", "city": "Pune"}
try:
print(data["age"])
except KeyError:
print("Key not found.")
except IndexError:
print("Index not found.")
finally:
print("Lookup attempt finished.")
Walk through it: data["age"] is attempted, and since "age" was never stored as a key in data, Python raises KeyError: 'age'. Python checks the except clauses in order; except KeyError is listed first and matches immediately, so it prints Key not found. — the IndexError clause below it is never even examined, because the search stopped at the first match. The finally block then runs regardless, printing Lookup attempt finished. The output is two lines: Key not found. followed by Lookup attempt finished.
scores = [67, 89, 72]
index = 3
try:
print(scores[index] * 2)
except IndexError:
print("No score at that position.")
else:
print("Calculation succeeded.")
Here, scores holds valid positions 0, 1, and 2 only, and index is 3, so scores[3] raises IndexError: list index out of range before the multiplication by 2 ever happens. The matching except IndexError block runs, printing No score at that position., and because an exception did occur, the else block is skipped entirely. The only printed line is No score at that position.
Summary
An exception is Python's way of reporting that a specific operation could not be completed while the program was running — distinct from a syntax error, which stops the program before it even starts, and a logical error, which produces a wrong answer with no complaint at all. A try block holds the code that might fail; each except block after it is a backup plan for one named type of failure, tried in the order they are written, so specific types must come before general ones or the general clause will steal every match. An else block runs only when the try block completes with no exception at all, and a finally block runs unconditionally, success or failure, making it the natural place for cleanup or closing messages. You can trigger an exception yourself with raise, attaching a message that explains exactly what rule was broken, and you can even define your own exception classes by inheriting from Exception when a built-in name does not describe your situation precisely. Programs that use these tools do not stop existing the moment a user does something unexpected — they respond, recover, and keep running, which is the actual difference between a script you write for yourself and software you can hand to someone else.
Practice Exercises
Now it is time to practice! Complete these challenges to solidify your understanding:
- Exercise 1: Write a short program that demonstrates the core concept from this chapter. Test it with at least 3 different inputs.
- Exercise 2: Find a real-world example where error handling: making robust python programs is used in an Indian company (like TCS, Infosys, Flipkart, or ISRO). Write a paragraph explaining the connection.
- Exercise 3: Create a mind-map connecting error handling: making robust python programs to at least 3 other topics you have studied.